use rand::rngs::StdRng;
use rand::SeedableRng;
use rustsim::prelude::*;
use std::time::Instant;
const N: u64 = 1_000_000;
#[derive(Debug, Clone)]
struct Particle {
id: AgentId,
x: f32,
vx: f32,
}
impl Agent for Particle {
fn id(&self) -> AgentId {
self.id
}
}
impl SoaExtractable for Particle {
fn num_columns() -> usize {
2
}
fn column_names() -> Vec<&'static str> {
vec!["x", "vx"]
}
fn extract_row(&self, columns: &mut [Vec<f32>]) {
columns[0].push(self.x);
columns[1].push(self.vx);
}
fn write_back_row(&mut self, columns: &[&[f32]], row: usize) {
self.x = columns[0][row];
}
}
fn integrate_cpu(columns: &mut [Vec<f32>], n: usize) {
let (x_col, rest) = columns.split_at_mut(1);
let x = &mut x_col[0];
let vx = &rest[0];
for i in 0..n {
x[i] += vx[i];
}
}
#[test]
fn gpu_accelerated_million_agents() {
let mut store = HashMapStore::new();
for i in 1..=N {
store.insert(Particle {
id: i,
x: 0.0,
vx: 0.001,
});
}
let backend = detect_backend();
eprintln!("[GPU test] Detected backend: {}", backend);
let t0 = Instant::now();
for step in 0..10 {
let result = cpu_batch_step::<Particle, _, _>(&store, integrate_cpu);
if step == 0 {
eprintln!(
"[GPU test] Step {} via {}: {} agents, kernel {} us",
step, result.backend, result.agent_count, result.kernel_us
);
}
}
let total_ms = t0.elapsed().as_millis();
let per_step = total_ms as f64 / 10.0;
eprintln!(
"[GPU test] 10 steps x {} agents in {} ms ({:.1} ms/step)",
N, total_ms, per_step
);
let a1 = store.get(1).unwrap();
let expected = 10.0 * 0.001f32;
assert!(
(a1.x - expected).abs() < 1e-5,
"agent 1: expected x={}, got x={}",
expected,
a1.x
);
drop(a1);
let mid = store.get(500_000).unwrap();
assert!(
(mid.x - expected).abs() < 1e-5,
"agent 500000: expected x={}, got x={}",
expected,
mid.x
);
drop(mid);
eprintln!("[GPU test] Correctness verified.");
assert!(
per_step < 5_000.0,
"batch step should take < 5s (took {:.0} ms)",
per_step
);
}
#[test]
fn soa_vs_aos_comparison() {
let mut store_soa = HashMapStore::new();
for i in 1..=N {
store_soa.insert(Particle {
id: i,
x: 0.0,
vx: 0.001,
});
}
let t_soa = Instant::now();
for _ in 0..10 {
cpu_batch_step::<Particle, _, _>(&store_soa, integrate_cpu);
}
let soa_ms = t_soa.elapsed().as_millis();
type PM = StandardModel<
rustsim_spaces::nothing::NothingSpace,
Particle,
HashMapStore<Particle>,
(),
StdRng,
Fastest,
>;
fn particle_step(
agent: &mut Particle,
_ctx: &mut StepContext<
'_,
rustsim_spaces::nothing::NothingSpace,
Particle,
(),
StdRng,
Fastest,
>,
) {
agent.x += agent.vx;
}
let mut store_aos = HashMapStore::new();
for i in 1..=N {
store_aos.insert(Particle {
id: i,
x: 0.0,
vx: 0.001,
});
}
let mut model = PM::new(
store_aos,
rustsim_spaces::nothing::NothingSpace,
Fastest::new(),
(),
StdRng::seed_from_u64(42),
Some(Box::new(particle_step)),
None,
true,
);
let t_aos = Instant::now();
model.step_n(10);
let aos_ms = t_aos.elapsed().as_millis();
eprintln!(
"[SoA vs AoS] 10 steps x {} agents: SoA={} ms, AoS={} ms",
N, soa_ms, aos_ms
);
let expected = 10.0 * 0.001f32;
let soa_x = store_soa.get(1).unwrap().x;
let aos_x = model.agent(1).unwrap().x;
assert!(
(soa_x - expected).abs() < 1e-5,
"SoA agent 1 wrong: {}",
soa_x
);
assert!(
(aos_x - expected).abs() < 1e-5,
"AoS agent 1 wrong: {}",
aos_x
);
eprintln!("[SoA vs AoS] Both paths produce correct results.");
}