plotter sim support via deepseek
This commit is contained in:
parent
db234a0a53
commit
c24a30ac31
1 changed files with 189 additions and 0 deletions
|
|
@ -372,6 +372,16 @@ impl WeaponType {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/// The radius of the weapon's ball, if it has one. Used to report the
|
||||||
|
/// swept parameter in sequential-sweep JSON output.
|
||||||
|
pub fn radius(&self) -> Option<f32> {
|
||||||
|
match self {
|
||||||
|
WeaponType::Ball { radius } => Some(*radius),
|
||||||
|
WeaponType::ForwardBall { radius } => Some(*radius),
|
||||||
|
WeaponType::Lazer { .. } => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn crater(&self, robot: &Robot) -> (f32, f32, f32, Color) {
|
pub fn crater(&self, robot: &Robot) -> (f32, f32, f32, Color) {
|
||||||
match self {
|
match self {
|
||||||
WeaponType::Ball{radius} => {
|
WeaponType::Ball{radius} => {
|
||||||
|
|
@ -631,6 +641,149 @@ fn run_headless_simulation(seed: u64, config: &SimConfig) -> (i32, i32) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Stopping parameters for [`run_sequential`].
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
struct SeqOptions {
|
||||||
|
/// Stop once `|mean| / standard_error >= sigma`.
|
||||||
|
sigma: f64,
|
||||||
|
/// Never check the stopping rule before this many samples.
|
||||||
|
min_samples: u64,
|
||||||
|
/// Hard cap on samples, whether or not the rule fired.
|
||||||
|
max_samples: u64,
|
||||||
|
/// Seeds evaluated per parallel batch between stopping-rule checks.
|
||||||
|
batch: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Outcome of a sequential sweep for a single configuration. `winner` is the
|
||||||
|
/// side the food-share difference favours; `stopped` records whether that
|
||||||
|
/// decision came from the sigma rule or from hitting `max_samples` (in which
|
||||||
|
/// case the match is effectively undecided).
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
struct SeqResult {
|
||||||
|
n: u64,
|
||||||
|
mean_red: f64,
|
||||||
|
mean_blu: f64,
|
||||||
|
mean_diff: f64,
|
||||||
|
sd_diff: f64,
|
||||||
|
se_diff: f64,
|
||||||
|
sigma: f64,
|
||||||
|
winner: &'static str,
|
||||||
|
stopped: &'static str,
|
||||||
|
red_wins: u64,
|
||||||
|
blu_wins: u64,
|
||||||
|
ties: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sequentially evaluate a single configuration, stopping as soon as the
|
||||||
|
/// RED-BLU food-share difference is `sigma` standard errors away from zero (or
|
||||||
|
/// `max_samples` is reached). Seeds run in parallel batches; every batch
|
||||||
|
/// reuses the same seed range starting at 0, so all configurations share
|
||||||
|
/// common random numbers and remain directly comparable.
|
||||||
|
fn run_sequential(config: &SimConfig, opts: SeqOptions) -> SeqResult {
|
||||||
|
let denom = config.food_amount.max(1) as f64;
|
||||||
|
let mut red = Variance::default();
|
||||||
|
let mut blu = Variance::default();
|
||||||
|
let mut diff = Variance::default();
|
||||||
|
let (mut red_wins, mut blu_wins, mut ties) = (0u64, 0u64, 0u64);
|
||||||
|
let mut n = 0u64;
|
||||||
|
let mut stopped = "max_samples";
|
||||||
|
|
||||||
|
while n < opts.max_samples {
|
||||||
|
let take = opts.batch.max(1).min(opts.max_samples - n);
|
||||||
|
let batch: Vec<(i32, i32)> = (n..n + take)
|
||||||
|
.into_par_iter()
|
||||||
|
.map(|seed| run_headless_simulation(seed, config))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
for &(r, b) in &batch {
|
||||||
|
let r_share = r as f64 / denom;
|
||||||
|
let b_share = b as f64 / denom;
|
||||||
|
red.push(r_share);
|
||||||
|
blu.push(b_share);
|
||||||
|
diff.push(r_share - b_share);
|
||||||
|
match r.cmp(&b) {
|
||||||
|
std::cmp::Ordering::Greater => red_wins += 1,
|
||||||
|
std::cmp::Ordering::Less => blu_wins += 1,
|
||||||
|
std::cmp::Ordering::Equal => ties += 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
n += take;
|
||||||
|
|
||||||
|
if n >= opts.min_samples {
|
||||||
|
if let Some(se) = diff.std_err() {
|
||||||
|
if se > 0.0 && (diff.mean.abs() / se) >= opts.sigma {
|
||||||
|
stopped = "sigma";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let sd = diff.std_dev().unwrap_or(0.0);
|
||||||
|
let se = diff.std_err().unwrap_or(0.0);
|
||||||
|
let sigma = if se > 0.0 { diff.mean.abs() / se } else { 0.0 };
|
||||||
|
|
||||||
|
SeqResult {
|
||||||
|
n,
|
||||||
|
mean_red: red.mean,
|
||||||
|
mean_blu: blu.mean,
|
||||||
|
mean_diff: diff.mean,
|
||||||
|
sd_diff: sd,
|
||||||
|
se_diff: se,
|
||||||
|
sigma,
|
||||||
|
winner: if diff.mean >= 0.0 { "red" } else { "blu" },
|
||||||
|
stopped,
|
||||||
|
red_wins,
|
||||||
|
blu_wins,
|
||||||
|
ties,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Format a float as a JSON number, falling back to `null` for non-finite
|
||||||
|
/// values so downstream `json` parsers never see `NaN`/`inf`.
|
||||||
|
fn json_float(x: f64) -> String {
|
||||||
|
if x.is_finite() {
|
||||||
|
format!("{x}")
|
||||||
|
} else {
|
||||||
|
"null".to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Emit one line of JSON describing the configuration and its sequential
|
||||||
|
/// result. Flat object, hand-rolled to avoid pulling in `serde_json`.
|
||||||
|
fn print_seq_json(config: &SimConfig, res: &SeqResult) {
|
||||||
|
let radius = match config.weapon.model.radius() {
|
||||||
|
Some(r) => json_float(r as f64),
|
||||||
|
None => "null".to_string(),
|
||||||
|
};
|
||||||
|
let fields: Vec<(&str, String)> = vec![
|
||||||
|
("view_dist", json_float(config.view_dist as f64)),
|
||||||
|
("weapon_radius", radius),
|
||||||
|
("weapon_kills_user", config.weapon.kills_user.to_string()),
|
||||||
|
("red_count", config.red_count.to_string()),
|
||||||
|
("blu_count", config.blu_count.to_string()),
|
||||||
|
("food_amount", config.food_amount.to_string()),
|
||||||
|
("n", res.n.to_string()),
|
||||||
|
("mean_red", json_float(res.mean_red)),
|
||||||
|
("mean_blu", json_float(res.mean_blu)),
|
||||||
|
("mean_diff", json_float(res.mean_diff)),
|
||||||
|
("sd_diff", json_float(res.sd_diff)),
|
||||||
|
("se_diff", json_float(res.se_diff)),
|
||||||
|
("sigma", json_float(res.sigma)),
|
||||||
|
("winner", format!("\"{}\"", res.winner)),
|
||||||
|
("stopped", format!("\"{}\"", res.stopped)),
|
||||||
|
("red_wins", res.red_wins.to_string()),
|
||||||
|
("blu_wins", res.blu_wins.to_string()),
|
||||||
|
("ties", res.ties.to_string()),
|
||||||
|
];
|
||||||
|
let body = fields
|
||||||
|
.iter()
|
||||||
|
.map(|(k, v)| format!("\"{k}\":{v}"))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(",");
|
||||||
|
println!("{{{body}}}");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[command(version, about, long_about = None)]
|
#[command(version, about, long_about = None)]
|
||||||
|
|
@ -651,6 +804,27 @@ struct Cli {
|
||||||
#[arg(short, long, value_name = "FILE")]
|
#[arg(short, long, value_name = "FILE")]
|
||||||
config: PathBuf,
|
config: PathBuf,
|
||||||
|
|
||||||
|
/// Sequential mode: run seeds in batches, stop once the RED-BLU food-share
|
||||||
|
/// difference is `--sigma` standard errors from zero, print JSON, exit.
|
||||||
|
#[arg(long)]
|
||||||
|
sequential: bool,
|
||||||
|
|
||||||
|
/// Sigma threshold for `--sequential` stopping (default 3).
|
||||||
|
#[arg(long, default_value_t = 3.0)]
|
||||||
|
sigma: f64,
|
||||||
|
|
||||||
|
/// Minimum seeds before the `--sequential` stopping rule may fire (default 32).
|
||||||
|
#[arg(long, default_value_t = 32)]
|
||||||
|
min_samples: u64,
|
||||||
|
|
||||||
|
/// Hard cap on seeds for `--sequential` (default 2000).
|
||||||
|
#[arg(long, default_value_t = 2000)]
|
||||||
|
max_samples: u64,
|
||||||
|
|
||||||
|
/// Seeds per parallel batch for `--sequential` (default 64).
|
||||||
|
#[arg(long, default_value_t = 64)]
|
||||||
|
batch: u64,
|
||||||
|
|
||||||
/// Turn debugging information on
|
/// Turn debugging information on
|
||||||
#[arg(short, long, action = clap::ArgAction::Count)]
|
#[arg(short, long, action = clap::ArgAction::Count)]
|
||||||
debug: u8,
|
debug: u8,
|
||||||
|
|
@ -750,6 +924,21 @@ fn print_average(red: i32, blu: i32, iters: u64) {
|
||||||
fn main() {
|
fn main() {
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
|
|
||||||
|
// Sequential mode is inherently headless: it never touches the GUI and
|
||||||
|
// prints a single JSON line per invocation.
|
||||||
|
if cli.sequential {
|
||||||
|
let config = load_config(&cli.config);
|
||||||
|
let opts = SeqOptions {
|
||||||
|
sigma: cli.sigma,
|
||||||
|
min_samples: cli.min_samples,
|
||||||
|
max_samples: cli.max_samples.max(1),
|
||||||
|
batch: cli.batch,
|
||||||
|
};
|
||||||
|
let result = run_sequential(&config, opts);
|
||||||
|
print_seq_json(&config, &result);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Headless runs never need a window. Going through macroquad's event loop
|
// Headless runs never need a window. Going through macroquad's event loop
|
||||||
// would otherwise cost seconds of X11/window setup on every invocation,
|
// would otherwise cost seconds of X11/window setup on every invocation,
|
||||||
// which dwarfs the actual simulation for parallel sweeps.
|
// which dwarfs the actual simulation for parallel sweeps.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue