fix(experiment): score occupied perimeter cells including one-column cycles

This commit is contained in:
2026-09-24 21:11:19 -07:00
parent 5300a8c803
commit ea18adc061
@@ -54,23 +54,49 @@ fn components(arcs: &[BTreeSet<usize>], work: &mut usize) -> Result<Vec<Vec<usiz
Ok(result)
}
// Clockwise perimeter slots. Restrict to near-square physical envelopes when
// possible; minimizing perimeter area without this restriction makes a strip.
fn perimeter_slots(n: usize, rows: usize, cols: usize) -> Vec<(usize, usize)> {
let mut slots = Vec::new();
if rows == 1 {
slots.extend((0..cols).map(|col| (col, 0)));
} else if cols == 1 {
slots.extend((0..rows).map(|row| (0, row)));
} else {
slots.extend((0..cols).map(|col| (col, 0)));
slots.extend((1..rows).map(|row| (cols - 1, row)));
slots.extend((0..cols - 1).rev().map(|col| (col, rows - 1)));
slots.extend((1..rows - 1).rev().map(|row| (0, row)));
}
slots.truncate(n);
slots
}
// Score occupied maximum-member cells, excluding unused perimeter slots.
// Heterogeneous members may occupy less than this nominal cell envelope.
fn perimeter(
n: usize,
cell: Size,
gap: Size,
work: &mut usize,
) -> Result<(Vec<Position>, usize, usize), ModelError> {
let mut best: Option<((bool, f32, f32, usize, usize), usize, usize)> = None;
for rows in 2..=n.max(2) {
for cols in 2..=n.max(2) {
type GridScore = (bool, f32, f32, usize, usize);
let mut best: Option<(GridScore, usize, usize)> = None;
for rows in 1..=n {
for cols in 1..=n {
charge(work, 1)?;
if 2 * rows + 2 * cols - 4 < n {
let capacity = if rows == 1 || cols == 1 {
rows * cols
} else {
2 * rows + 2 * cols - 4
};
if capacity < n {
continue;
}
let width = cols as f32 * cell.width + (cols - 1) as f32 * gap.width;
let height = rows as f32 * cell.height + (rows - 1) as f32 * gap.height;
charge(work, capacity)?;
let slots = perimeter_slots(n, rows, cols);
let max_x = slots.iter().map(|p| p.0).max().unwrap();
let max_y = slots.iter().map(|p| p.1).max().unwrap();
let width = (max_x + 1) as f32 * cell.width + max_x as f32 * gap.width;
let height = (max_y + 1) as f32 * cell.height + max_y as f32 * gap.height;
let aspect = (width / height).max(height / width);
let key = (
aspect > 2.0,
@@ -89,19 +115,7 @@ fn perimeter(
}
}
let (_, rows, cols) = best.ok_or_else(|| error("no perimeter grid"))?;
let mut slots = Vec::new();
for col in 0..cols {
slots.push((col, 0));
}
for row in 1..rows {
slots.push((cols - 1, row));
}
for col in (0..cols - 1).rev() {
slots.push((col, rows - 1));
}
for row in (1..rows - 1).rev() {
slots.push((0, row));
}
let slots = perimeter_slots(n, rows, cols);
Ok((
slots
.into_iter()
@@ -548,4 +562,27 @@ mod tests {
}
}
}
#[test]
fn two_node_perimeter_scores_occupied_cells_instead_of_an_unused_row() {
let (positions, rows, cols) = perimeter(
2,
Size {
width: 224.0,
height: 160.0,
},
Size {
width: 250.0,
height: 56.0,
},
&mut 0,
)
.unwrap();
assert_eq!((rows, cols), (2, 1));
assert_eq!(
positions,
vec![Position { x: 0.0, y: 0.0 }, Position { x: 0.0, y: 216.0 }]
);
assert!(224.0_f32 / 376.0 > 0.5);
}
}