#![cfg(feature = "training")]
use burn::{
backend::{Autodiff, NdArray, ndarray::NdArrayDevice},
optim::AdamConfig,
};
use thrust_rl::{
env::games::matching_pennies::MatchingPennies,
multi_agent::{
FictitiousPlayMetaSolver, JointTrainerConfig, MetaSolver, NfspConfig, NfspTrainer,
PsroConfig, PsroTrainer,
},
policy::mlp::MlpBurnPolicy,
train::optimizer::BurnOptimizer,
};
type B = Autodiff<NdArray<f32>>;
fn psro_exploitability_trace(seed: u64) -> Vec<f32> {
let device: NdArrayDevice = Default::default();
let psro_config = PsroConfig {
max_iterations: 6,
max_population_size: 50,
br_train_steps_per_iteration: 2,
payoff_eval_episodes: 4,
max_payoff_evals_per_iteration: None,
br_reward_scale: 1.0,
seed,
serialize_br_updates: true,
};
let joint_config = JointTrainerConfig {
num_agents: 2,
rollout_steps: 32,
n_epochs: 1,
minibatch_size: 32,
..Default::default()
};
let mut trainer = PsroTrainer::new(
psro_config,
joint_config,
Box::new(FictitiousPlayMetaSolver::new(500)) as Box<dyn MetaSolver>,
device,
|dev: &NdArrayDevice, s: u64| {
MlpBurnPolicy::<B>::new_seeded(
MatchingPennies::OBS_DIM,
MatchingPennies::ACTION_DIM,
16,
s,
dev,
)
},
|| BurnOptimizer::new(AdamConfig::new().init(), 1e-3),
MatchingPennies::new,
)
.expect("PsroTrainer::new should succeed");
let stats = trainer.run_silent().expect("PSRO run should not error");
stats.iterations.iter().map(|it| it.exploitability).collect()
}
#[test]
fn test_psro_exploitability_trace_is_bit_identical() {
let a = psro_exploitability_trace(42);
let b = psro_exploitability_trace(42);
assert_eq!(a.len(), 6);
assert_eq!(a, b, "same PsroConfig::seed must yield a bit-identical exploitability trace");
let c = psro_exploitability_trace(43);
assert_ne!(a, c, "different seed should change the trace");
}
#[allow(clippy::type_complexity)]
fn nfsp_avg_marginal_trace(seed: u64) -> Vec<f32> {
let device: NdArrayDevice = Default::default();
let nfsp_config = NfspConfig {
max_iterations: 6,
anticipatory_param: 0.1,
reservoir_capacity: 4_096,
br_train_steps_per_iteration: 1,
avg_policy_train_steps_per_iteration: 4,
avg_policy_minibatch_size: 32,
avg_policy_lr: 5e-3,
avg_policy_min_reservoir_coverage: 0.0,
br_reward_scale: 1.0,
seed,
};
let joint_config = JointTrainerConfig {
num_agents: 2,
rollout_steps: 64,
n_epochs: 1,
minibatch_size: 32,
..Default::default()
};
let mut trainer: NfspTrainer<
B,
MlpBurnPolicy<B>,
burn::optim::adaptor::OptimizerAdaptor<burn::optim::Adam, MlpBurnPolicy<B>, B>,
MatchingPennies,
_,
_,
_,
> = NfspTrainer::new(
nfsp_config,
joint_config,
device,
|dev: &NdArrayDevice, s: u64| {
MlpBurnPolicy::<B>::new_seeded(
MatchingPennies::OBS_DIM,
MatchingPennies::ACTION_DIM,
16,
s,
dev,
)
},
|| BurnOptimizer::new(AdamConfig::new().init(), 5e-3),
MatchingPennies::new,
)
.expect("NfspTrainer::new should succeed");
let stats = trainer.run_silent().expect("NFSP run should not error");
let mut trace = Vec::new();
for it in &stats.iterations {
if let Some(Some(m)) = it.avg_action_marginal.first() {
trace.extend_from_slice(m);
}
}
trace
}
#[test]
fn test_nfsp_avg_marginal_trace_is_bit_identical() {
let a = nfsp_avg_marginal_trace(7);
let b = nfsp_avg_marginal_trace(7);
assert!(!a.is_empty(), "trace should be non-empty");
assert_eq!(a, b, "same NfspConfig::seed must yield a bit-identical marginal trace");
let c = nfsp_avg_marginal_trace(8);
assert_ne!(a, c, "different seed should change the trace");
}
#[test]
fn test_nfsp_avg_marginal_stream_matches_golden_reference() {
let golden: Vec<f32> = vec![
0.46875_f32,
0.53125_f32,
0.5625_f32,
0.4375_f32,
0.5703125_f32,
0.4296875_f32,
0.625_f32,
0.375_f32,
0.5234375_f32,
0.4765625_f32,
0.5390625_f32,
0.4609375_f32,
];
let trace = nfsp_avg_marginal_trace(7);
assert_eq!(
trace, golden,
"NFSP avg-marginal action stream (seed 7) must be bit-identical to the pre-#235 \
reference; the batched seeded sampler must not change the RNG draw order"
);
}