#![allow(clippy::cast_precision_loss, clippy::cast_possible_truncation)]
use crate::splitmix::{GAMMA, finalize, fold};
use core::convert::Infallible;
use logaddexp::LogAddExp;
use rand::TryRng;
use rand_distr::{Distribution, weighted::WeightedAliasIndex};
#[derive(Debug)]
pub(super) struct DetRng(u64);
impl DetRng {
fn step(&mut self) -> u64 {
self.0 = self.0.wrapping_add(GAMMA);
finalize(self.0)
}
}
impl TryRng for DetRng {
type Error = Infallible;
fn try_next_u32(&mut self) -> Result<u32, Infallible> {
Ok((self.step() >> 32) as u32)
}
fn try_next_u64(&mut self) -> Result<u64, Infallible> {
Ok(self.step())
}
fn try_fill_bytes(&mut self, dst: &mut [u8]) -> Result<(), Infallible> {
let (chunks, rem) = dst.as_chunks_mut::<8>();
for chunk in chunks {
*chunk = self.step().to_le_bytes();
}
if !rem.is_empty() {
let bytes = self.step().to_le_bytes();
rem.copy_from_slice(&bytes[..rem.len()]);
}
Ok(())
}
}
#[derive(Debug, Clone, Copy)]
pub(super) struct SampleKey(u64);
impl SampleKey {
pub(super) fn new(seed: u64, iteration: u64, sweep: u64) -> Self {
SampleKey(fold(fold(seed, iteration), sweep))
}
pub(super) fn rng(self, infoset: u32) -> DetRng {
DetRng(fold(self.0, u64::from(infoset)))
}
}
#[derive(Debug)]
pub struct SampledChance {
index: WeightedAliasIndex<f64>,
}
impl SampledChance {
pub fn new(probs: &[f64]) -> Self {
SampledChance {
index: WeightedAliasIndex::new(probs.to_vec()).unwrap(),
}
}
pub(super) fn sample(&self, rng: &mut DetRng) -> usize {
self.index.sample(rng)
}
}
#[derive(Debug, Clone, Default)]
pub(super) struct Action {
cum_regret: f32,
strat: f32,
pub(super) cum_strat: f64,
}
impl Action {
fn with_strat(strat: f32) -> Action {
Action {
strat,
..Default::default()
}
}
pub(super) fn regret(&self) -> f64 {
self.cum_regret.into()
}
pub(super) fn set_regret(&mut self, value: f64) {
self.cum_regret = value as f32;
}
pub(super) fn add_regret(&mut self, value: f64) {
self.set_regret(self.regret() + value);
}
pub(super) fn strat(&self) -> f64 {
self.strat.into()
}
pub(super) fn set_strat(&mut self, value: f64) {
self.strat = value as f32;
}
}
#[derive(Debug)]
pub struct RegretInfoset {
pub(super) actions: Box<[Action]>,
}
impl RegretInfoset {
pub fn new(num_actions: usize) -> Self {
debug_assert_eq!(size_of::<Action>(), 16, "Action should stay packed");
let uniform = 1.0 / num_actions as f32;
RegretInfoset {
actions: (0..num_actions)
.map(|_| Action::with_strat(uniform))
.collect(),
}
}
pub(super) fn strat(&self) -> impl ExactSizeIterator<Item = f32> + '_ {
self.actions.iter().map(|action| action.strat)
}
pub fn into_avg_strat(self) -> Box<[f64]> {
let mut avg: Box<[f64]> = self.actions.iter().map(|action| action.cum_strat).collect();
avg_strat(&mut avg);
avg
}
}
pub fn avg_strat(cum_strat: &mut [f64]) {
let norm: f64 = cum_strat.iter().sum();
if norm == 0.0 {
cum_strat.fill(1.0 / cum_strat.len() as f64);
} else {
for prob in cum_strat.iter_mut() {
*prob /= norm;
}
}
}
pub type SolveInfo = ([f64; 2], [Box<[f64]>; 2]);
pub(super) fn should_check(it: u64, max_iter: u64, check_interval: u64) -> bool {
it.is_multiple_of(check_interval) || it == max_iter
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct RegretParams {
pub pos_regret: f64,
pub neg_regret: f64,
pub strat: f64,
pub no_positive: f64,
}
impl RegretParams {
#[must_use]
pub fn new(pos_regret: f64, neg_regret: f64, strat: f64, no_positive: f64) -> Self {
assert!(
!pos_regret.is_nan(),
"positive regret discounting can't be nan"
);
assert!(
!neg_regret.is_nan(),
"negative regret discounting can't be nan"
);
assert!(
strat >= 0.0,
"strategy discounting must be non-negative: {strat}"
);
assert!(
strat != f64::INFINITY,
"strategy discounting must be finite",
);
assert!(
!no_positive.is_nan(),
"no positive regret weight can't be nan"
);
RegretParams {
pos_regret,
neg_regret,
strat,
no_positive,
}
}
#[must_use]
pub fn vanilla() -> Self {
RegretParams {
pos_regret: f64::INFINITY,
neg_regret: f64::INFINITY,
strat: 0.0,
no_positive: 0.0,
}
}
#[must_use]
pub fn lcfr() -> Self {
RegretParams {
pos_regret: 1.0,
neg_regret: 1.0,
strat: 1.0,
no_positive: f64::INFINITY,
}
}
#[must_use]
pub fn cfr_plus() -> Self {
RegretParams {
pos_regret: f64::INFINITY,
neg_regret: f64::NEG_INFINITY,
strat: 2.0,
no_positive: f64::INFINITY,
}
}
#[must_use]
pub fn dcfr() -> Self {
RegretParams {
pos_regret: 1.5,
neg_regret: 0.0,
strat: 2.0,
no_positive: f64::INFINITY,
}
}
#[must_use]
pub fn dcfr_prune() -> Self {
RegretParams {
pos_regret: 1.5,
neg_regret: 0.5,
strat: 2.0,
no_positive: f64::INFINITY,
}
}
pub(super) fn regret_match(&self, actions: &mut [Action]) {
let norm: f64 = actions
.iter()
.map(Action::regret)
.filter(|value| *value > 0.0)
.sum();
if norm > 0.0 {
for action in actions.iter_mut() {
action.set_strat(if action.regret() > 0.0 {
action.regret() / norm
} else {
0.0
});
}
} else if self.no_positive == f64::INFINITY {
let ind = actions
.iter()
.enumerate()
.max_by(|(_, l), (_, r)| l.regret().partial_cmp(&r.regret()).unwrap())
.unwrap()
.0;
for (i, action) in actions.iter_mut().enumerate() {
action.set_strat(f64::from(i == ind));
}
} else if self.no_positive == 0.0 {
let uniform = 1.0 / actions.len() as f64;
for action in actions.iter_mut() {
action.set_strat(uniform);
}
} else if self.no_positive == f64::NEG_INFINITY {
let ind = actions
.iter()
.enumerate()
.min_by(|(_, l), (_, r)| l.regret().partial_cmp(&r.regret()).unwrap())
.unwrap()
.0;
for (i, action) in actions.iter_mut().enumerate() {
action.set_strat(f64::from(i == ind));
}
} else {
let max = actions.iter().map(Action::regret).reduce(f64::max).unwrap();
let norm: f64 = actions
.iter()
.map(|action| ((action.regret() - max) * self.no_positive).exp())
.sum();
for action in actions.iter_mut() {
action.set_strat(((action.regret() - max) * self.no_positive).exp() / norm);
}
}
}
pub(super) fn gen_discount(it: u64, discount: f64) -> f64 {
if discount == f64::NEG_INFINITY {
0.0
} else if discount == 0.0 {
0.5
} else if discount == f64::INFINITY {
1.0
} else {
let numer = discount * (it as f64).ln();
let denom = numer.ln_add_exp(0.0);
(numer - denom).exp()
}
}
pub(crate) fn iteration_factors(&self, it: u64) -> (f64, f64, f64) {
let strat = if self.strat == f64::INFINITY {
0.0
} else if self.strat > 0.0 {
let float = it as f64;
(float / (float + 1.0)).powf(self.strat)
} else {
1.0
};
(
RegretParams::gen_discount(it, self.pos_regret),
RegretParams::gen_discount(it, self.neg_regret),
strat,
)
}
}
#[derive(Debug)]
enum StratDiscount {
Forget,
Scale(f64),
Keep,
}
#[derive(Debug)]
pub(super) struct Discounts<'a> {
params: &'a RegretParams,
it: u64,
pos: f64,
neg: f64,
strat: StratDiscount,
}
impl<'a> Discounts<'a> {
pub(super) fn new(params: &'a RegretParams, it: u64, strat_it: u64) -> Self {
let strat = if params.strat == f64::INFINITY {
StratDiscount::Forget
} else if params.strat > 0.0 {
let float = strat_it as f64;
StratDiscount::Scale((float / (float + 1.0)).powf(params.strat))
} else {
StratDiscount::Keep
};
Discounts {
params,
it,
pos: RegretParams::gen_discount(it, params.pos_regret),
neg: RegretParams::gen_discount(it, params.neg_regret),
strat,
}
}
pub(super) fn advance_infoset(&self, actions: &mut [Action]) -> f64 {
let positive_norm: f64 = actions
.iter()
.map(Action::regret)
.filter(|value| *value > 0.0)
.sum();
if positive_norm > 0.0 {
let mut max = f64::NEG_INFINITY;
for action in actions.iter_mut() {
let cur = action.regret();
action.set_strat(if cur > 0.0 { cur / positive_norm } else { 0.0 });
let discounted = if cur > 0.0 {
cur * self.pos
} else if cur < 0.0 {
cur * self.neg
} else {
cur
};
action.set_regret(discounted);
max = f64::max(max, action.regret());
}
2.0 * f64::max(max, 0.0) / self.it as f64
} else {
self.params.regret_match(actions);
self.discount_cum_regret(actions);
self.regret_bound(actions)
}
}
pub(super) fn discount_cum_regret(&self, actions: &mut [Action]) {
for action in actions.iter_mut() {
if action.regret() > 0.0 {
action.set_regret(action.regret() * self.pos);
} else if action.regret() < 0.0 {
action.set_regret(action.regret() * self.neg);
}
}
}
pub(super) fn discount_average_strat(&self, actions: &mut [Action]) {
match self.strat {
StratDiscount::Forget => {
for action in actions.iter_mut() {
action.cum_strat = 0.0;
}
}
StratDiscount::Scale(ratio) => {
for action in actions.iter_mut() {
action.cum_strat *= ratio;
}
}
StratDiscount::Keep => {}
}
}
pub(super) fn regret_bound(&self, actions: &[Action]) -> f64 {
2.0 * f64::max(
actions
.iter()
.map(Action::regret)
.reduce(f64::max)
.unwrap_or(0.0),
0.0,
) / self.it as f64
}
}
impl Default for RegretParams {
fn default() -> Self {
Self::dcfr()
}
}
impl Eq for RegretParams {}
#[cfg(test)]
#[allow(
clippy::float_cmp,
clippy::similar_names,
clippy::cast_precision_loss,
clippy::cast_lossless,
unused_must_use
)]
mod tests {
use super::{Action, Discounts, RegretParams};
fn from_regret(regrets: &[f32]) -> Vec<Action> {
regrets
.iter()
.map(|&cum_regret| Action {
cum_regret,
strat: 0.0,
cum_strat: 0.0,
})
.collect()
}
fn from_cum_strat(cum_strats: &[f64]) -> Vec<Action> {
cum_strats
.iter()
.map(|&cum_strat| Action {
cum_regret: 0.0,
strat: 0.0,
cum_strat,
})
.collect()
}
fn strats(actions: &[Action]) -> Vec<f32> {
actions.iter().map(|action| action.strat).collect()
}
fn regrets(actions: &[Action]) -> Vec<f32> {
actions.iter().map(|action| action.cum_regret).collect()
}
fn cum_strats(actions: &[Action]) -> Vec<f64> {
actions.iter().map(|action| action.cum_strat).collect()
}
#[test]
fn avg_strat() {
let mut strat = [1.0, 2.0, 1.0];
super::avg_strat(&mut strat);
assert_eq!(strat, [0.25, 0.5, 0.25]);
}
#[test]
fn regret_match() {
let mut acts = from_regret(&[1.0, 2.0, 1.0, -3.0]);
RegretParams::new(0.0, 0.0, 0.0, 0.0).regret_match(&mut acts);
assert_eq!(strats(&acts), [0.25, 0.5, 0.25, 0.0]);
let mut acts = from_regret(&[-1.0, -2.0]);
RegretParams::new(0.0, 0.0, 0.0, 0.0).regret_match(&mut acts);
assert_eq!(strats(&acts), [0.5, 0.5]);
RegretParams::new(0.0, 0.0, 0.0, f64::INFINITY).regret_match(&mut acts);
assert_eq!(strats(&acts), [1.0, 0.0]);
RegretParams::new(0.0, 0.0, 0.0, f64::NEG_INFINITY).regret_match(&mut acts);
assert_eq!(strats(&acts), [0.0, 1.0]);
RegretParams::new(0.0, 0.0, 0.0, 1.0).regret_match(&mut acts);
let strat = strats(&acts);
assert!((0.6..0.9).contains(&strat[0]));
assert!((0.1..0.4).contains(&strat[1]));
RegretParams::new(0.0, 0.0, 0.0, -1.0).regret_match(&mut acts);
let strat = strats(&acts);
assert!((0.1..0.4).contains(&strat[0]));
assert!((0.6..0.9).contains(&strat[1]));
}
#[test]
fn discount_regs() {
let mut acts = from_regret(&[1.0, -1.0]);
Discounts::new(&RegretParams::new(0.0, 0.0, 0.0, 0.0), 2, 2).discount_cum_regret(&mut acts);
assert_eq!(regrets(&acts), [0.5, -0.5]);
let mut acts = from_regret(&[1.0, -1.0]);
Discounts::new(
&RegretParams::new(f64::INFINITY, f64::NEG_INFINITY, 0.0, 0.0),
2,
2,
)
.discount_cum_regret(&mut acts);
assert_eq!(regrets(&acts), [1.0, 0.0]);
let mut acts = from_regret(&[1.0, -1.0]);
Discounts::new(
&RegretParams::new(f64::NEG_INFINITY, f64::INFINITY, 0.0, 0.0),
2,
2,
)
.discount_cum_regret(&mut acts);
assert_eq!(regrets(&acts), [0.0, -1.0]);
let mut acts = from_regret(&[1.0, -1.0]);
Discounts::new(&RegretParams::new(1.0, 1.0, 0.0, 0.0), 2, 2).discount_cum_regret(&mut acts);
let regret = regrets(&acts);
assert!((0.6..0.9).contains(®ret[0]), "{}", regret[0]);
assert!((-0.9..-0.6).contains(®ret[1]), "{}", regret[1]);
}
#[test]
fn discount_average_strat() {
let mut acts = from_cum_strat(&[1.0, 2.0]);
Discounts::new(&RegretParams::new(0.0, 0.0, 0.0, 0.0), 1, 1)
.discount_average_strat(&mut acts);
assert_eq!(cum_strats(&acts), [1.0, 2.0]);
let mut acts = from_cum_strat(&[1.0, 2.0]);
Discounts::new(&RegretParams::lcfr(), 1, 1).discount_average_strat(&mut acts);
assert_eq!(cum_strats(&acts), [0.5, 1.0]);
let mut acts = from_cum_strat(&[1.0, 2.0]);
Discounts::new(&RegretParams::cfr_plus(), 1, 1).discount_average_strat(&mut acts);
assert_eq!(cum_strats(&acts), [0.25, 0.5]);
let mut acts = from_cum_strat(&[1.0, 2.0]);
Discounts::new(&RegretParams::lcfr(), 2, 2).discount_average_strat(&mut acts);
let cum_strat = cum_strats(&acts);
assert!((0.6..0.9).contains(&cum_strat[0]), "{}", cum_strat[0]);
assert!((1.1..1.9).contains(&cum_strat[1]), "{}", cum_strat[1]);
let params = RegretParams::lcfr();
let mut acts = from_cum_strat(&[0.0]);
for t in 1..=5 {
acts[0].cum_strat += 1.0;
Discounts::new(¶ms, t, t).discount_average_strat(&mut acts);
}
let expected = (1..=5).sum::<usize>() as f64 / (5 + 1) as f64;
assert!((cum_strats(&acts)[0] - expected).abs() < 1e-6, "{}", cum_strats(&acts)[0]);
}
#[test]
fn regret_bound() {
let acts = from_regret(&[1.0, 2.0, 1.0, -3.0]);
let res = Discounts::new(&RegretParams::vanilla(), 1, 1).regret_bound(&acts);
assert_eq!(res, 4.0);
let res = Discounts::new(&RegretParams::vanilla(), 2, 2).regret_bound(&acts);
assert_eq!(res, 2.0);
let res = Discounts::new(&RegretParams::vanilla(), 4, 4).regret_bound(&acts);
assert_eq!(res, 1.0);
}
#[test]
fn advance_infoset_matches_unfused() {
let cases = [
vec![1.0, 2.0, 1.0, -3.0], vec![5.0, 0.0, -1.0], vec![-1.0, -2.0, -0.5], vec![0.0, 0.0], vec![3.0], ];
let params_list = [
RegretParams::vanilla(),
RegretParams::dcfr(),
RegretParams::cfr_plus(),
RegretParams::lcfr(),
];
for params in ¶ms_list {
for it in [1_u64, 2, 7] {
for case in &cases {
let discounts = Discounts::new(params, it, it);
let mut fused = from_regret(case);
let fused_bound = discounts.advance_infoset(&mut fused);
let mut acts = from_regret(case);
params.regret_match(&mut acts);
discounts.discount_cum_regret(&mut acts);
let bound = discounts.regret_bound(&acts);
assert_eq!(strats(&fused), strats(&acts), "strat {params:?} it={it} {case:?}");
assert_eq!(
regrets(&fused),
regrets(&acts),
"cum_regret {params:?} it={it} {case:?}"
);
assert_eq!(fused_bound, bound, "bound {params:?} it={it} {case:?}");
}
}
}
}
#[test]
#[should_panic(expected = "positive regret")]
fn nan_pos() {
RegretParams::new(f64::NAN, 0.0, 0.0, 0.0);
}
#[test]
#[should_panic(expected = "negative regret")]
fn nan_neg() {
RegretParams::new(0.0, f64::NAN, 0.0, 0.0);
}
#[test]
#[should_panic(expected = "strategy discounting")]
fn nan_strat() {
RegretParams::new(0.0, 0.0, f64::NAN, 0.0);
}
#[test]
#[should_panic(expected = "strategy discounting")]
fn neg_strat() {
RegretParams::new(0.0, 0.0, -1.0, 0.0);
}
#[test]
#[should_panic(expected = "strategy discounting")]
fn inf_strat() {
RegretParams::new(0.0, 0.0, f64::INFINITY, 0.0);
}
#[test]
#[should_panic(expected = "no positive regret weight")]
fn nan_no_pos() {
RegretParams::new(0.0, 0.0, 0.0, f64::NAN);
}
}