Skip to content

Commit e2102ba

Browse files
committed
test: benches and round-trip coverage for diffusion/decay art knobs
1 parent fb3f865 commit e2102ba

2 files changed

Lines changed: 249 additions & 1 deletion

File tree

benches/diffusion_benchmark.rs

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,10 +193,77 @@ fn bench_diffuse_comparison(c: &mut Criterion) {
193193
group.finish();
194194
}
195195

196+
fn bench_diffuse_gaussian_separable_sigma3(c: &mut Criterion) {
197+
use tslime::simulation::trail_map::TrailMap;
198+
199+
let width = 400;
200+
let height = 400;
201+
let mut tm = TrailMap::new(width, height);
202+
let data = tm.current_mut();
203+
for (i, v) in data.iter_mut().enumerate() {
204+
*v = (i * 7 % 100) as f32 / 10.0;
205+
}
206+
207+
c.bench_function("diffuse_gaussian_separable_sigma3", |b| {
208+
b.iter(|| {
209+
tm.diffuse_gaussian_separable(black_box(3.0));
210+
});
211+
});
212+
}
213+
214+
fn bench_decay_gamma(c: &mut Criterion) {
215+
use tslime::simulation::trail_map::TrailMap;
216+
217+
let width = 400;
218+
let height = 400;
219+
let mut tm = TrailMap::new(width, height);
220+
let data = tm.current_mut();
221+
for (i, v) in data.iter_mut().enumerate() {
222+
*v = (i * 7 % 100) as f32 / 10.0;
223+
}
224+
225+
c.bench_function("decay_gamma_0.9_0.5", |b| {
226+
b.iter(|| {
227+
tm.decay_gamma(black_box(0.9), black_box(0.5));
228+
});
229+
});
230+
}
231+
232+
fn bench_afterglow_ema_pass(c: &mut Criterion) {
233+
use tslime::simulation::config::{InitMode, SimConfig, SpeciesConfig};
234+
use tslime::Simulation;
235+
236+
let config = SimConfig {
237+
afterglow: 0.4,
238+
afterglow_rate: 0.05,
239+
species_configs: vec![SpeciesConfig {
240+
count: 100,
241+
..Default::default()
242+
}],
243+
..Default::default()
244+
};
245+
let mut sim = Simulation::new(400, 400, config, 42, InitMode::Random, 0);
246+
// Enable afterglow EMA buffer (normally done by app/runner when afterglow > 0).
247+
sim.set_compute_afterglow(true, 0.05);
248+
// Warm up the trail map so the EMA has non-zero values to process.
249+
for _ in 0..10 {
250+
sim.update(1.0);
251+
}
252+
253+
c.bench_function("afterglow_ema_pass", |b| {
254+
b.iter(|| {
255+
sim.update(black_box(1.0));
256+
});
257+
});
258+
}
259+
196260
criterion_group!(
197261
benches,
198262
bench_diffuse_mean3x3,
199263
bench_diffuse_gaussian,
200-
bench_diffuse_comparison
264+
bench_diffuse_comparison,
265+
bench_diffuse_gaussian_separable_sigma3,
266+
bench_decay_gamma,
267+
bench_afterglow_ema_pass
201268
);
202269
criterion_main!(benches);

src/config_manager.rs

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1299,4 +1299,185 @@ init_mode = "Random"
12991299
assert!((new_state.temporal_lag_frames - 12.0).abs() < 1e-6);
13001300
assert_eq!(new_state.temporal_mode, TemporalMode::Accent);
13011301
}
1302+
1303+
#[test]
1304+
fn diffusion_decay_art_knobs_round_trip() {
1305+
// Verify all five diffusion/decay art knobs survive from_runtime →
1306+
// TOML serialize → TOML deserialize → apply_to_runtime_state.
1307+
let mut state = create_test_runtime_state();
1308+
state.afterglow = 0.4;
1309+
state.afterglow_rate = 0.03;
1310+
state.decay_gamma = 0.6;
1311+
state.diffuse_weight = 0.5;
1312+
state.diffusion_sigma = 3.0;
1313+
1314+
let sim_config = SimConfig {
1315+
sensor_angle: state.sensor_angle,
1316+
sensor_distance: 9.0,
1317+
rotation_angle: state.rotation_angle,
1318+
step_size: state.step_size,
1319+
decay_factor: state.decay_factor,
1320+
deposit_amount: state.deposit_amount,
1321+
diffusion_kernel: state.diffusion_kernel,
1322+
diffusion_sigma: state.diffusion_sigma,
1323+
afterglow: state.afterglow,
1324+
afterglow_rate: state.afterglow_rate,
1325+
diffuse_weight: state.diffuse_weight,
1326+
decay_gamma: state.decay_gamma,
1327+
max_brightness: state.max_brightness,
1328+
time_scale: 1.0,
1329+
attractors: Vec::new(),
1330+
attractor_strength: 1.0,
1331+
mouse_attractors: Vec::new(),
1332+
mouse_timeout: 3.0,
1333+
species_configs: vec![SpeciesConfig {
1334+
name: "default".to_string(),
1335+
count: 1000,
1336+
sensor_angle: state.sensor_angle,
1337+
rotation_angle: state.rotation_angle,
1338+
step_size: state.step_size,
1339+
deposit_amount: state.deposit_amount,
1340+
color: RgbColor::from_hex(0x228b22),
1341+
trail_modulation: None,
1342+
}],
1343+
separate_species_trails: false,
1344+
use_simd: true,
1345+
food_image_path: None,
1346+
food_image_invert: false,
1347+
food_image_scale: 1.0,
1348+
obstacles: Vec::new(),
1349+
obstacle_masks: Vec::new(),
1350+
wind: None,
1351+
terrain: crate::simulation::config::TerrainType::None,
1352+
terrain_strength: 1.0,
1353+
background_color: None,
1354+
preferred_init_mode: None,
1355+
boundary_mode: crate::simulation::config::BoundaryMode::Bounce,
1356+
respawn_config: crate::simulation::config::RespawnConfig::default(),
1357+
sampling_mode: crate::simulation::config::SamplingMode::Nearest,
1358+
window_frame: crate::simulation::config::WindowFrame::None,
1359+
chrome_style: crate::simulation::config::ChromeStyle::default(),
1360+
aspect: crate::simulation::config::Aspect::default(),
1361+
window_padding: crate::simulation::config::WindowPadding::default(),
1362+
show_status_bar: false,
1363+
min_sim_size: crate::simulation::config::TerminalSizeThreshold::default(),
1364+
min_frame_size: crate::simulation::config::TerminalSizeThreshold {
1365+
width: 12,
1366+
height: 6,
1367+
},
1368+
};
1369+
1370+
let saved = SavedConfig::from_runtime(
1371+
"art_knobs_rt".to_string(),
1372+
&sim_config,
1373+
crate::cli::Palette::Organic,
1374+
crate::render::charset::Charset::HalfBlock,
1375+
false,
1376+
false,
1377+
0,
1378+
false,
1379+
false,
1380+
false,
1381+
None,
1382+
crate::simulation::config::InitMode::Random,
1383+
None,
1384+
None,
1385+
0.0,
1386+
8.0,
1387+
crate::render::palette::TemporalMode::Hue,
1388+
state.afterglow,
1389+
state.afterglow_rate,
1390+
state.decay_gamma,
1391+
state.diffuse_weight,
1392+
);
1393+
1394+
// Serialize and deserialize through TOML
1395+
let toml_str = toml::to_string(&saved).expect("serialize must succeed");
1396+
let reloaded: SavedConfig = toml::from_str(&toml_str).expect("deserialize must succeed");
1397+
1398+
// Restore into a fresh RuntimeState
1399+
let mut new_state = create_test_runtime_state();
1400+
reloaded
1401+
.apply_to_runtime_state(&mut new_state)
1402+
.expect("apply must succeed");
1403+
1404+
assert!(
1405+
(new_state.afterglow - 0.4).abs() < 1e-6,
1406+
"afterglow must survive round-trip (got {})",
1407+
new_state.afterglow
1408+
);
1409+
assert!(
1410+
(new_state.afterglow_rate - 0.03).abs() < 1e-6,
1411+
"afterglow_rate must survive round-trip (got {})",
1412+
new_state.afterglow_rate
1413+
);
1414+
assert!(
1415+
(new_state.decay_gamma - 0.6).abs() < 1e-6,
1416+
"decay_gamma must survive round-trip (got {})",
1417+
new_state.decay_gamma
1418+
);
1419+
assert!(
1420+
(new_state.diffuse_weight - 0.5).abs() < 1e-6,
1421+
"diffuse_weight must survive round-trip (got {})",
1422+
new_state.diffuse_weight
1423+
);
1424+
assert!(
1425+
(new_state.diffusion_sigma - 3.0).abs() < 1e-6,
1426+
"diffusion_sigma must survive round-trip (got {})",
1427+
new_state.diffusion_sigma
1428+
);
1429+
}
1430+
1431+
#[test]
1432+
fn old_toml_without_art_knobs_loads_with_defaults() {
1433+
// An OLD TOML without afterglow/decay_gamma/diffuse_weight must still
1434+
// deserialize, and apply_to_runtime_state must produce the canonical
1435+
// defaults (afterglow=0.0, afterglow_rate=0.05, decay_gamma=1.0,
1436+
// diffuse_weight=1.0).
1437+
let toml = r#"name = "old_no_knobs"
1438+
population = 1000
1439+
sensor_angle = 22.5
1440+
sensor_distance = 9.0
1441+
rotation_angle = 45.0
1442+
step_size = 1.0
1443+
decay_factor = 0.9
1444+
deposit_amount = 5.0
1445+
max_brightness = 100.0
1446+
diffusion_kernel = "Mean3x3"
1447+
diffusion_sigma = 1.0
1448+
palette = "Organic"
1449+
charset = "HalfBlock"
1450+
reverse_palette = false
1451+
invert_palette = false
1452+
warmup_frames = 0
1453+
food_persist = false
1454+
auto_reset = false
1455+
grid = false
1456+
init_mode = "Random"
1457+
"#;
1458+
let cfg: SavedConfig =
1459+
toml::from_str(toml).expect("old config without art knobs must load");
1460+
assert!(
1461+
cfg.afterglow.is_none(),
1462+
"missing key must deserialize as None"
1463+
);
1464+
assert!(cfg.afterglow_rate.is_none());
1465+
assert!(cfg.decay_gamma.is_none());
1466+
assert!(cfg.diffuse_weight.is_none());
1467+
1468+
// apply_to_runtime_state must fill defaults from the unwrap_or paths.
1469+
let mut state = create_test_runtime_state();
1470+
cfg.apply_to_runtime_state(&mut state)
1471+
.expect("legacy config must still apply");
1472+
assert_eq!(state.afterglow, 0.0, "default afterglow must be 0.0");
1473+
assert!(
1474+
(state.afterglow_rate - 0.05).abs() < 1e-6,
1475+
"default afterglow_rate must be 0.05"
1476+
);
1477+
assert_eq!(state.decay_gamma, 1.0, "default decay_gamma must be 1.0");
1478+
assert_eq!(
1479+
state.diffuse_weight, 1.0,
1480+
"default diffuse_weight must be 1.0"
1481+
);
1482+
}
13021483
}

0 commit comments

Comments
 (0)