From 071450d09bc6102a737737aa082d5d8f84a128c6 Mon Sep 17 00:00:00 2001 From: Paul Date: Mon, 11 May 2026 13:24:13 -0600 Subject: [PATCH 01/40] ArrayIndex stores z value Co-authored-by: Copilot --- crates/cli/src/main.rs | 4 +- crates/revrt/benches/standard.rs | 24 ++-- crates/revrt/src/dataset/mod.rs | 74 +++++------ crates/revrt/src/dataset/reader.rs | 40 +++--- crates/revrt/src/lib.rs | 116 +++++++++++------- crates/revrt/src/network/cost.rs | 100 +++++++-------- crates/revrt/src/network/long_range/mod.rs | 92 +++++++++----- .../revrt/src/network/long_range/utilities.rs | 23 ++-- crates/revrt/src/network/unused.rs | 54 ++++---- crates/revrt/src/routing/astar.rs | 18 ++- crates/revrt/src/routing/long_range.rs | 50 ++++---- crates/revrt/src/routing/mod.rs | 15 ++- crates/revrt/src/routing/scenario.rs | 18 +-- tests/rust/integration_tests.rs | 8 +- 14 files changed, 362 insertions(+), 274 deletions(-) diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 2b41c454..063baa58 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -54,11 +54,11 @@ fn main() { trace!("User given dataset: {:?}", cli.dataset); assert_eq!(cli.start.len(), 2); - let start = revrt::ArrayIndex::new(cli.start[0] as u64, cli.start[1] as u64); + let start = revrt::ArrayIndex::new_ij(cli.start[0] as u64, cli.start[1] as u64); trace!("Starting point: {:?}", start); assert_eq!(cli.end.len(), 2); - let end = vec![revrt::ArrayIndex::new(cli.end[0] as u64, cli.end[1] as u64)]; + let end = vec![revrt::ArrayIndex::new_ij(cli.end[0] as u64, cli.end[1] as u64)]; trace!("Ending point: {:?}", end); let result = resolve( diff --git a/crates/revrt/benches/standard.rs b/crates/revrt/benches/standard.rs index 9c61c05c..aff450c6 100644 --- a/crates/revrt/benches/standard.rs +++ b/crates/revrt/benches/standard.rs @@ -103,8 +103,8 @@ fn standard_ones(c: &mut Criterion) { b.iter(|| { bench_minimalist( black_box(features_path.clone()), - black_box(vec![ArrayIndex::new(20, 50)]), - black_box(vec![ArrayIndex::new(5, 50)]), + black_box(vec![ArrayIndex::new_ij(20, 50)]), + black_box(vec![ArrayIndex::new_ij(5, 50)]), ) }) }); @@ -118,8 +118,8 @@ fn standard_random(c: &mut Criterion) { b.iter(|| { bench_minimalist( black_box(features_path.clone()), - black_box(vec![ArrayIndex::new(20, 50)]), - black_box(vec![ArrayIndex::new(5, 50)]), + black_box(vec![ArrayIndex::new_ij(20, 50)]), + black_box(vec![ArrayIndex::new_ij(5, 50)]), ) }) }); @@ -135,10 +135,10 @@ fn multiple_near_routes(c: &mut Criterion) { black_box(features_path.clone()), black_box( (19..=22) - .flat_map(|row| (48..=51).map(move |col| ArrayIndex::new(row, col))) + .flat_map(|row| (48..=51).map(move |col| ArrayIndex::new_ij(row, col))) .collect::>(), ), - black_box(vec![ArrayIndex::new(10, 50)]), + black_box(vec![ArrayIndex::new_ij(10, 50)]), ) }) }); @@ -158,11 +158,11 @@ fn multiple_spread_routes(c: &mut Criterion) { .flat_map(|row| { (40..=60) .step_by(5) - .map(move |col| ArrayIndex::new(row, col)) + .map(move |col| ArrayIndex::new_ij(row, col)) }) .collect::>(), ), - black_box(vec![ArrayIndex::new(50, 50)]), + black_box(vec![ArrayIndex::new_ij(50, 50)]), ) }) }); @@ -176,8 +176,8 @@ fn single_chunk(c: &mut Criterion) { b.iter(|| { bench_minimalist( black_box(features_path.clone()), - black_box(vec![ArrayIndex::new(20, 50)]), - black_box(vec![ArrayIndex::new(5, 50)]), + black_box(vec![ArrayIndex::new_ij(20, 50)]), + black_box(vec![ArrayIndex::new_ij(5, 50)]), ) }) }); @@ -199,8 +199,8 @@ fn range_distance(c: &mut Criterion) { b.iter(|| { bench_minimalist( black_box(features_path.clone()), - black_box(vec![ArrayIndex::new(X0 + distance, 50)]), - black_box(vec![ArrayIndex::new(X0, 50)]), + black_box(vec![ArrayIndex::new_ij(X0 + distance, 50)]), + black_box(vec![ArrayIndex::new_ij(X0, 50)]), ) }) }, diff --git a/crates/revrt/src/dataset/mod.rs b/crates/revrt/src/dataset/mod.rs index b9809a29..9d1d0d0c 100644 --- a/crates/revrt/src/dataset/mod.rs +++ b/crates/revrt/src/dataset/mod.rs @@ -244,7 +244,7 @@ mod tests { let dataset = Dataset::open(tmp.path(), cost_function, 1_000).expect("Error opening dataset"); - let test_points = [ArrayIndex { i: 3, j: 1 }, ArrayIndex { i: 2, j: 2 }]; + let test_points = [ArrayIndex::new_ij(3, 1), ArrayIndex::new_ij(2, 2)]; let array = zarrs::array::Array::open(dataset.source.clone(), "/A").unwrap(); for point in test_points { let results = dataset.get_3x3(&point); @@ -253,9 +253,9 @@ mod tests { assert!( !results .iter() - .any(|(ArrayIndex { i, j }, _)| *i == 0 && *j == 0) + .any(|(ArrayIndex { i, j, .. }, _)| *i == 0 && *j == 0) ); - let ArrayIndex { i: ci, j: cj } = point; + let ArrayIndex { i: ci, j: cj, .. } = point; let center_subset = zarrs::array_subset::ArraySubset::new_with_ranges(&[ 0..1, ci..(ci + 1), @@ -265,7 +265,7 @@ mod tests { .retrieve_array_subset_elements(¢er_subset) .expect("Error reading zarr data")[0]; - for (ArrayIndex { i, j }, val) in results { + for (ArrayIndex { i, j, .. }, val) in results { let subset = zarrs::array_subset::ArraySubset::new_with_ranges(&[ 0..1, i..(i + 1), @@ -338,12 +338,12 @@ mod tests { let dataset = Dataset::open(tmp.path(), cost_function, 1_000).expect("Error opening dataset"); - let test_points = [ArrayIndex { i: 3, j: 1 }, ArrayIndex { i: 2, j: 2 }]; + let test_points = [ArrayIndex::new_ij(3, 1), ArrayIndex::new_ij(2, 2)]; let array = zarrs::array::Array::open(dataset.source.clone(), "/A").unwrap(); for point in test_points { let results = dataset.get_3x3(&point); - for (ArrayIndex { i, j }, val) in results { + for (ArrayIndex { i, j, .. }, val) in results { let subset = zarrs::array_subset::ArraySubset::new_with_ranges(&[ 0..1, i..(i + 1), @@ -365,7 +365,7 @@ mod tests { let dataset = Dataset::open(tmp.path(), cost_function, 1_000).expect("Error opening dataset"); - let test_points = [ArrayIndex { i: 3, j: 1 }, ArrayIndex { i: 2, j: 2 }]; + let test_points = [ArrayIndex::new_ij(3, 1), ArrayIndex::new_ij(2, 2)]; let array_a = zarrs::array::Array::open(dataset.source.clone(), "/A").unwrap(); let array_b = zarrs::array::Array::open(dataset.source.clone(), "/B").unwrap(); let array_c = zarrs::array::Array::open(dataset.source.clone(), "/C").unwrap(); @@ -376,9 +376,9 @@ mod tests { assert!( !results .iter() - .any(|(ArrayIndex { i, j }, _)| *i == 0 && *j == 0) + .any(|(ArrayIndex { i, j, .. }, _)| *i == 0 && *j == 0) ); - let ArrayIndex { i: ci, j: cj } = point; + let ArrayIndex { i: ci, j: cj, .. } = point; let center_subset = zarrs::array_subset::ArraySubset::new_with_ranges(&[ 0..1, ci..(ci + 1), @@ -397,7 +397,7 @@ mod tests { let center_cost: f32 = center_a + center_b * 100. + center_a * center_b + center_c * center_a * 2.; - for (ArrayIndex { i, j }, val) in results { + for (ArrayIndex { i, j, .. }, val) in results { let subset = zarrs::array_subset::ArraySubset::new_with_ranges(&[ 0..1, i..(i + 1), @@ -450,13 +450,13 @@ mod tests { let dataset = Dataset::open(tmp.path(), cost_function, 1_000).expect("Error opening dataset"); - let results = dataset.get_3x3(&ArrayIndex { i: 0, j: 0 }); + let results = dataset.get_3x3(&ArrayIndex::new_ij(0, 0)); // index 0, 0 has a cost of 0 and should therefore be filtered out assert!( !results .iter() - .any(|(ArrayIndex { i, j }, _)| *i == 0 && *j == 0) + .any(|(ArrayIndex { i, j, .. }, _)| *i == 0 && *j == 0) ); assert_eq!(results, vec![]); @@ -473,20 +473,20 @@ mod tests { let dataset = Dataset::open(tmp.path(), cost_function, 1_000).expect("Error opening dataset"); - let results = dataset.get_3x3(&ArrayIndex { i: si, j: sj }); + let results = dataset.get_3x3(&ArrayIndex::new_ij(si, sj)); // index 0, 0 has a cost of 0 and should therefore be filtered out assert!( !results .iter() - .any(|(ArrayIndex { i, j }, _)| *i == 0 && *j == 0) + .any(|(ArrayIndex { i, j, .. }, _)| *i == 0 && *j == 0) ); assert_eq!( results, expected_output .into_iter() - .map(|(i, j, v)| (ArrayIndex { i, j }, v)) + .map(|(i, j, v)| (ArrayIndex::new_ij(i, j), v)) .collect::>() ); } @@ -510,20 +510,20 @@ mod tests { let dataset = Dataset::open(tmp.path(), cost_function, 1_000).expect("Error opening dataset"); - let results = dataset.get_3x3(&ArrayIndex { i: si, j: sj }); + let results = dataset.get_3x3(&ArrayIndex::new_ij(si, sj)); // index 0, 0 has a cost of 0 and should therefore be filtered out assert!( !results .iter() - .any(|(ArrayIndex { i, j }, _)| *i == 0 && *j == 0) + .any(|(ArrayIndex { i, j, .. }, _)| *i == 0 && *j == 0) ); assert_eq!( results, expected_output .into_iter() - .map(|(i, j, v)| (ArrayIndex { i, j }, v)) + .map(|(i, j, v)| (ArrayIndex::new_ij(i, j), v)) .collect::>() ); } @@ -550,20 +550,20 @@ mod tests { let dataset = Dataset::open(tmp.path(), cost_function, 1_000).expect("Error opening dataset"); - let results = dataset.get_3x3(&ArrayIndex { i: si, j: sj }); + let results = dataset.get_3x3(&ArrayIndex::new_ij(si, sj)); // index 0, 0 has a cost of 0 and should therefore be filtered out assert!( !results .iter() - .any(|(ArrayIndex { i, j }, _)| *i == 0 && *j == 0) + .any(|(ArrayIndex { i, j, .. }, _)| *i == 0 && *j == 0) ); assert_eq!( results, expected_output .into_iter() - .map(|(i, j, v)| (ArrayIndex { i, j }, v)) + .map(|(i, j, v)| (ArrayIndex::new_ij(i, j), v)) .collect::>() ); } @@ -596,7 +596,7 @@ mod tests { Dataset::open(tmp.path(), cost_function, 1_000).expect("Error opening dataset"); // Request center neighbors - let point = ArrayIndex { i: 1, j: 1 }; + let point = ArrayIndex::new_ij(1, 1); let results = dataset.get_3x3(&point); // Build expected results: for each neighbor (excluding center), @@ -637,7 +637,7 @@ mod tests { let total_before = averaged + c_n; let friction = b_n * 0.5_f32; let expected_val = total_before * (1.0_f32 + friction); - expected.push((ArrayIndex { i: ir, j: jr }, expected_val)); + expected.push((ArrayIndex::new_ij(ir, jr), expected_val)); } } @@ -676,7 +676,7 @@ mod tests { let dataset = Dataset::open(tmp.path(), cost_function, 1_000).expect("Error opening dataset"); - let results = dataset.get_3x3(&ArrayIndex { i: 1, j: 1 }); + let results = dataset.get_3x3(&ArrayIndex::new_ij(1, 1)); assert!( results.is_empty(), "Found data with `ignore_invalid_costs=true`" @@ -698,7 +698,7 @@ mod tests { let dataset = Dataset::open(tmp.path(), cost_function, 1_000).expect("Error opening dataset"); - let results = dataset.get_3x3(&ArrayIndex { i: 1, j: 1 }); + let results = dataset.get_3x3(&ArrayIndex::new_ij(1, 1)); assert_eq!(results.len(), 8); let mut expected: Vec<(ArrayIndex, f32)> = vec![]; @@ -712,7 +712,7 @@ mod tests { if ir != 1 && jr != 1 { averaged *= std::f32::consts::SQRT_2; } - expected.push((ArrayIndex { i: ir, j: jr }, averaged)); + expected.push((ArrayIndex::new_ij(ir, jr), averaged)); } } @@ -763,14 +763,14 @@ mod tests { let dataset = Dataset::open(tmp.path(), cost_function, 1_000).expect("Error opening dataset"); - let results = dataset.get_3x3(&ArrayIndex { i: 1, j: 1 }); + let results = dataset.get_3x3(&ArrayIndex::new_ij(1, 1)); assert_eq!( results, vec![ - (ArrayIndex { i: 0, j: 0 }, 3.0 * std::f32::consts::SQRT_2), - (ArrayIndex { i: 0, j: 2 }, 4.0 * std::f32::consts::SQRT_2), - (ArrayIndex { i: 2, j: 0 }, 6.0 * std::f32::consts::SQRT_2), - (ArrayIndex { i: 2, j: 2 }, 7.0 * std::f32::consts::SQRT_2), + (ArrayIndex::new_ij(0, 0), 3.0 * std::f32::consts::SQRT_2), + (ArrayIndex::new_ij(0, 2), 4.0 * std::f32::consts::SQRT_2), + (ArrayIndex::new_ij(2, 0), 6.0 * std::f32::consts::SQRT_2), + (ArrayIndex::new_ij(2, 2), 7.0 * std::f32::consts::SQRT_2), ] ); } @@ -806,7 +806,7 @@ mod tests { let dataset = Dataset::open(tmp.path(), cost_function, 1_000).expect("Error opening dataset"); - let results = dataset.get_3x3(&ArrayIndex { i: 1, j: 1 }); + let results = dataset.get_3x3(&ArrayIndex::new_ij(1, 1)); assert_eq!(results.len(), 8); } @@ -849,16 +849,16 @@ mod tests { let dataset = Dataset::open(tmp.path(), CostFunction::from_json(json).unwrap(), 1_000) .expect("Error opening dataset"); - let center = ArrayIndex { i: 1, j: 1 }; + let center = ArrayIndex::new_ij(1, 1); dataset.get_3x3(¢er); assert_eq!( dataset.get_3x3_soft_barrier_cells(¢er, 0), - vec![ArrayIndex { i: 0, j: 1 }, ArrayIndex { i: 1, j: 0 }] + vec![ArrayIndex::new_ij(0, 1), ArrayIndex::new_ij(1, 0)] ); assert_eq!( dataset.get_3x3_soft_barrier_cells(¢er, 1), - vec![ArrayIndex { i: 0, j: 1 }] + vec![ArrayIndex::new_ij(0, 1)] ); assert!(dataset.get_3x3_soft_barrier_cells(¢er, 2).is_empty()); assert!(dataset.get_3x3_soft_barrier_cells(¢er, 99).is_empty()); @@ -903,12 +903,12 @@ mod tests { let dataset = Dataset::open(tmp.path(), CostFunction::from_json(json).unwrap(), 1_000) .expect("Error opening dataset"); - let center = ArrayIndex { i: 1, j: 1 }; + let center = ArrayIndex::new_ij(1, 1); dataset.get_3x3(¢er); assert_eq!( dataset.get_3x3_soft_barrier_cells(¢er, 0), - vec![ArrayIndex { i: 0, j: 1 }, ArrayIndex { i: 1, j: 0 }] + vec![ArrayIndex::new_ij(0, 1), ArrayIndex::new_ij(1, 0)] ); assert!(dataset.get_3x3_soft_barrier_cells(¢er, 1).is_empty()); } diff --git a/crates/revrt/src/dataset/reader.rs b/crates/revrt/src/dataset/reader.rs index 8a0c9718..e9f78087 100644 --- a/crates/revrt/src/dataset/reader.rs +++ b/crates/revrt/src/dataset/reader.rs @@ -263,7 +263,11 @@ impl NeighborhoodReader { .zip(barrier_values) { if is_barrier { - barrier_cells.push(ArrayIndex { i: ir, j: jr }); + barrier_cells.push(ArrayIndex { + i: ir, + j: jr, + option: index.option, + }); } } @@ -482,7 +486,7 @@ mod tests { ) { let reader = reader_for_grid(grid_nrows, grid_ncols); - let (i_range, j_range, subset) = reader.neighborhood_subset(&ArrayIndex { i, j }); + let (i_range, j_range, subset) = reader.neighborhood_subset(&ArrayIndex::new_ij(i, j)); assert_eq!(i_range, expected_i_range.clone()); assert_eq!(j_range, expected_j_range.clone()); @@ -507,19 +511,19 @@ mod tests { ); let neighbors = fixture.reader.get_3x3( - &ArrayIndex { i: 1, j: 1 }, + &ArrayIndex::new_ij(1, 1), &NoOpMaterializer { has_hard_barriers: true, }, ); let expected = [ - (ArrayIndex { i: 0, j: 0 }, 3.0 * SQRT_2 + 1.0), - (ArrayIndex { i: 0, j: 2 }, 4.0 * SQRT_2 + 1.0), - (ArrayIndex { i: 1, j: 0 }, 5.5), - (ArrayIndex { i: 2, j: 0 }, 6.0 * SQRT_2 + 1.0), - (ArrayIndex { i: 2, j: 1 }, 7.5), - (ArrayIndex { i: 2, j: 2 }, 7.0 * SQRT_2 + 1.0), + (ArrayIndex::new_ij(0, 0), 3.0 * SQRT_2 + 1.0), + (ArrayIndex::new_ij(0, 2), 4.0 * SQRT_2 + 1.0), + (ArrayIndex::new_ij(1, 0), 5.5), + (ArrayIndex::new_ij(2, 0), 6.0 * SQRT_2 + 1.0), + (ArrayIndex::new_ij(2, 1), 7.5), + (ArrayIndex::new_ij(2, 2), 7.0 * SQRT_2 + 1.0), ]; assert_eq!(neighbors.len(), expected.len()); @@ -542,7 +546,7 @@ mod tests { ); let neighbors = fixture.reader.get_3x3( - &ArrayIndex { i: 1, j: 1 }, + &ArrayIndex::new_ij(1, 1), &NoOpMaterializer { has_hard_barriers: true, }, @@ -560,7 +564,7 @@ mod tests { vec![false; 9], vec![false; 9], ); - let index = ArrayIndex { i: 1, j: 1 }; + let index = ArrayIndex::new_ij(1, 1); let (i_range, j_range, subset) = fixture.reader.neighborhood_subset(&index); let raw_costs = fixture @@ -591,10 +595,10 @@ mod tests { assert_eq!( neighbors, vec![ - (ArrayIndex { i: 0, j: 0 }, 3.0 * SQRT_2), - (ArrayIndex { i: 0, j: 2 }, 4.0 * SQRT_2), - (ArrayIndex { i: 2, j: 0 }, 6.0 * SQRT_2), - (ArrayIndex { i: 2, j: 2 }, 7.0 * SQRT_2), + (ArrayIndex::new_ij(0, 0), 3.0 * SQRT_2), + (ArrayIndex::new_ij(0, 2), 4.0 * SQRT_2), + (ArrayIndex::new_ij(2, 0), 6.0 * SQRT_2), + (ArrayIndex::new_ij(2, 2), 7.0 * SQRT_2), ] ); } @@ -610,14 +614,14 @@ mod tests { ); let retry_zero = fixture.reader.get_3x3_soft_barrier_cells( - &ArrayIndex { i: 1, j: 1 }, + &ArrayIndex::new_ij(1, 1), 0, &NoOpMaterializer { has_hard_barriers: false, }, ); let retry_one = fixture.reader.get_3x3_soft_barrier_cells( - &ArrayIndex { i: 1, j: 1 }, + &ArrayIndex::new_ij(1, 1), 1, &NoOpMaterializer { has_hard_barriers: false, @@ -626,7 +630,7 @@ mod tests { assert_eq!( retry_zero, - vec![ArrayIndex { i: 0, j: 1 }, ArrayIndex { i: 2, j: 0 }] + vec![ArrayIndex::new_ij(0, 1), ArrayIndex::new_ij(2, 0)] ); assert_eq!( retry_one, diff --git a/crates/revrt/src/lib.rs b/crates/revrt/src/lib.rs index 12da491b..e0a21dec 100644 --- a/crates/revrt/src/lib.rs +++ b/crates/revrt/src/lib.rs @@ -25,21 +25,32 @@ use solution::{RevrtRoutingSolutions, Solution}; pub struct ArrayIndex { i: u64, j: u64, + option: u32, } impl ArrayIndex { #[allow(missing_docs)] pub fn new(i: u64, j: u64) -> Self { - Self { i, j } + Self { i, j, option: 0 } + } + + pub fn new_ij(i: u64, j: u64) -> Self { + Self { i, j, option: 0 } } } impl From for (u64, u64) { - fn from(ArrayIndex { i, j }: ArrayIndex) -> (u64, u64) { + fn from(ArrayIndex { i, j, .. }: ArrayIndex) -> (u64, u64) { (i, j) } } +impl From for (u64, u64, u32) { + fn from(ArrayIndex { i, j, option }: ArrayIndex) -> (u64, u64, u32) { + (i, j, option) + } +} + #[allow(missing_docs)] pub fn resolve>( store_path: P, @@ -104,23 +115,46 @@ mod tests { #[test] fn tuple_from_index() { - let index_tuple: (u64, u64) = From::from(ArrayIndex { i: 2, j: 3 }); + let index_tuple: (u64, u64) = From::from(ArrayIndex::new_ij(2, 3)); assert_eq!(index_tuple.0, 2); assert_eq!(index_tuple.1, 3); } #[test] fn index_into_tuple() { - let index_tuple: (u64, u64) = ArrayIndex { i: 2, j: 3 }.into(); + let index_tuple: (u64, u64) = ArrayIndex::new_ij(2, 3).into(); assert_eq!(index_tuple.0, 2); assert_eq!(index_tuple.1, 3); } #[test] fn vec_contains_index() { - let vec_of_indices = [ArrayIndex { i: 2, j: 3 }, ArrayIndex { i: 5, j: 6 }]; - assert!(vec_of_indices.contains(&ArrayIndex { i: 5, j: 6 })); - assert!(!vec_of_indices.contains(&ArrayIndex { i: 8, j: 9 })); + let vec_of_indices = [ArrayIndex::new_ij(2, 3), ArrayIndex::new_ij(5, 6)]; + assert!(vec_of_indices.contains(&ArrayIndex::new_ij(5, 6))); + assert!(!vec_of_indices.contains(&ArrayIndex::new_ij(8, 9))); + } + + #[test] + fn index_with_option_into_tuple() { + let index_tuple: (u64, u64, u32) = ArrayIndex { + i: 2, + j: 3, + option: 4, + } + .into(); + assert_eq!(index_tuple, (2, 3, 4)); + } + + #[test] + fn index_with_option_still_converts_to_2d_tuple() { + let index_tuple: (u64, u64) = ArrayIndex { + i: 5, + j: 6, + option: 2, + } + .into(); + + assert_eq!(index_tuple, (5, 6)); } #[test_case("astar"; "astar")] @@ -135,8 +169,8 @@ mod tests { dataset::samples::multi_variable_random(1, 8, 8, 1, 4, 4, &["A", "B", "C", "cost"]); let cost_function = cost::sample::cost_function(); let mut simulation = Routing::new(&store_path, cost_function, 1_000, algorithm).unwrap(); - let start = vec![ArrayIndex { i: 2, j: 3 }]; - let end = vec![ArrayIndex { i: 6, j: 6 }]; + let start = vec![ArrayIndex::new_ij(2, 3)]; + let end = vec![ArrayIndex::new_ij(6, 6)]; let solutions = simulation.compute(&start, end).collect::>(); dbg!(&solutions); assert_eq!(solutions.len(), 1); @@ -182,8 +216,8 @@ mod tests { let cost_function = CostFunction::from_json(r#"{"cost_layers": [{"layer_name": "cost"}]}"#).unwrap(); let mut simulation = Routing::new(&store_path, cost_function, 1_000, algorithm).unwrap(); - let start = vec![ArrayIndex { i: si, j: sj }]; - let end = vec![ArrayIndex { i: ei, j: ej }]; + let start = vec![ArrayIndex::new_ij(si, sj)]; + let end = vec![ArrayIndex::new_ij(ei, ej)]; let solutions = simulation.compute(&start, end).collect::>(); dbg!(&solutions); assert_eq!(solutions.len(), 1); @@ -208,11 +242,11 @@ mod tests { let cost_function = CostFunction::from_json(r#"{"cost_layers": [{"layer_name": "cost"}]}"#).unwrap(); let mut simulation = Routing::new(&store_path, cost_function, 1_000, algorithm).unwrap(); - let start = vec![ArrayIndex { i: si, j: sj }]; + let start = vec![ArrayIndex::new_ij(si, sj)]; let end = endpoints .clone() .into_iter() - .map(|(i, j)| ArrayIndex { i, j }) + .map(|(i, j)| ArrayIndex::new_ij(i, j)) .collect(); let solutions = simulation.compute(&start, end).collect::>(); dbg!(&solutions); @@ -221,7 +255,7 @@ mod tests { assert_eq!(solutions[0].total_cost(), &expected_cost); assert_eq!(solutions[0].route()[0], start[0]); - let &ArrayIndex { i: ei, j: ej } = solutions[0].route().last().unwrap(); + let &ArrayIndex { i: ei, j: ej, .. } = solutions[0].route().last().unwrap(); assert_eq!((ei, ej), expected_endpoint); } @@ -254,8 +288,8 @@ mod tests { ) .unwrap(); let mut simulation = Routing::new(store_path, cost_function, 1_000, "dijkstra").unwrap(); - let start = vec![ArrayIndex { i: 1, j: 1 }]; - let end = vec![ArrayIndex { i: 0, j: 0 }]; + let start = vec![ArrayIndex::new_ij(1, 1)]; + let end = vec![ArrayIndex::new_ij(0, 0)]; let solutions = simulation.compute(&start, end).collect::>(); @@ -287,11 +321,11 @@ mod tests { let cost_function = CostFunction::from_json(r#"{"cost_layers": [{"layer_name": "cost"}]}"#).unwrap(); let mut simulation = Routing::new(&store_path, cost_function, 1_000, algorithm).unwrap(); - let start = vec![ArrayIndex { i: si, j: sj }]; + let start = vec![ArrayIndex::new_ij(si, sj)]; let end = endpoints .clone() .into_iter() - .map(|(i, j)| ArrayIndex { i, j }) + .map(|(i, j)| ArrayIndex::new_ij(i, j)) .collect(); let mut solutions = simulation.compute(&start, end).collect::>(); dbg!(&solutions); @@ -302,7 +336,7 @@ mod tests { assert_eq!(s.total_cost(), &(2. * cost_array_fill)); assert_eq!(s.route()[0], start[0]); - let &ArrayIndex { i: ei, j: ej } = s.route().last().unwrap(); + let &ArrayIndex { i: ei, j: ej, .. } = s.route().last().unwrap(); assert!(endpoints.contains(&(ei, ej))); } @@ -319,23 +353,23 @@ mod tests { CostFunction::from_json(r#"{"cost_layers": [{"layer_name": "cost"}]}"#).unwrap(); let mut simulation = Routing::new(&store_path, cost_function, 1_000, algorithm).unwrap(); let start = vec![ - ArrayIndex { i: 1, j: 1 }, - ArrayIndex { i: 3, j: 3 }, - ArrayIndex { i: 5, j: 5 }, + ArrayIndex::new_ij(1, 1), + ArrayIndex::new_ij(3, 3), + ArrayIndex::new_ij(5, 5), ]; let end = vec![ - ArrayIndex { i: 1, j: 2 }, - ArrayIndex { i: 4, j: 4 }, - ArrayIndex { i: 7, j: 7 }, + ArrayIndex::new_ij(1, 2), + ArrayIndex::new_ij(4, 4), + ArrayIndex::new_ij(7, 7), ]; let solutions = simulation.compute(&start, end).collect::>(); dbg!(&solutions); assert_eq!(solutions.len(), 3); let expected_solution = vec![ - (ArrayIndex { i: 1, j: 2 }, 1.0), - (ArrayIndex { i: 4, j: 4 }, 1.4142), - (ArrayIndex { i: 4, j: 4 }, 1.4142), + (ArrayIndex::new_ij(1, 2), 1.0), + (ArrayIndex::new_ij(4, 4), 1.4142), + (ArrayIndex::new_ij(4, 4), 1.4142), ]; for (s, eep) in solutions.into_iter().zip(expected_solution) { assert_eq!(s.route().len(), 2); @@ -354,8 +388,8 @@ mod tests { let cost_function = CostFunction::from_json(r#"{"cost_layers": [{"layer_name": "cost"}]}"#).unwrap(); let mut simulation = Routing::new(&store_path, cost_function, 1_000, algorithm).unwrap(); - let start = vec![ArrayIndex { i: 1, j: 1 }, ArrayIndex { i: 5, j: 5 }]; - let end = vec![ArrayIndex { i: 3, j: 3 }]; + let start = vec![ArrayIndex::new_ij(1, 1), ArrayIndex::new_ij(5, 5)]; + let end = vec![ArrayIndex::new_ij(3, 3)]; let solutions = simulation.compute(&start, end).collect::>(); dbg!(&solutions); assert_eq!(solutions.len(), 2); @@ -363,7 +397,7 @@ mod tests { for s in solutions { assert_eq!(s.route().len(), 3); assert_eq!(s.total_cost(), &2.8284); - assert_eq!(*s.route().last().unwrap(), ArrayIndex { i: 3, j: 3 }); + assert_eq!(*s.route().last().unwrap(), ArrayIndex::new_ij(3, 3)); } } @@ -394,8 +428,8 @@ mod tests { CostFunction::from_json(r#"{"cost_layers": [{"layer_name": "cost"}]}"#).unwrap(); let mut simulation = Routing::new(&store_path, cost_function, 1_000, algorithm).unwrap(); - let start = vec![ArrayIndex { i: 0, j: 0 }]; - let end = vec![ArrayIndex { i: 0, j: 2 }]; + let start = vec![ArrayIndex::new_ij(0, 0)]; + let end = vec![ArrayIndex::new_ij(0, 2)]; let mut solutions = simulation.compute(&start, end).collect::>(); assert_eq!(solutions.len(), 1); @@ -404,14 +438,14 @@ mod tests { assert_eq!(s.total_cost(), &8.2426); let expected_track = vec![ - ArrayIndex { i: 0, j: 0 }, - ArrayIndex { i: 1, j: 0 }, - ArrayIndex { i: 2, j: 0 }, - ArrayIndex { i: 3, j: 1 }, - ArrayIndex { i: 3, j: 2 }, - ArrayIndex { i: 2, j: 3 }, - ArrayIndex { i: 1, j: 3 }, - ArrayIndex { i: 0, j: 2 }, + ArrayIndex::new_ij(0, 0), + ArrayIndex::new_ij(1, 0), + ArrayIndex::new_ij(2, 0), + ArrayIndex::new_ij(3, 1), + ArrayIndex::new_ij(3, 2), + ArrayIndex::new_ij(2, 3), + ArrayIndex::new_ij(1, 3), + ArrayIndex::new_ij(0, 2), ]; assert_eq!(s.route(), &expected_track); } diff --git a/crates/revrt/src/network/cost.rs b/crates/revrt/src/network/cost.rs index 6e019385..5ab74ffb 100644 --- a/crates/revrt/src/network/cost.rs +++ b/crates/revrt/src/network/cost.rs @@ -132,12 +132,12 @@ mod test { #[test] fn equal_integers() { let a = NodeCost { - index: ArrayIndex::new(0, 0), + index: ArrayIndex::new_ij(0, 0), cost: 5, estimated_cost: 10, }; let b = NodeCost { - index: ArrayIndex::new(1, 1), + index: ArrayIndex::new_ij(1, 1), cost: 5, estimated_cost: 10, }; @@ -148,12 +148,12 @@ mod test { /// Order of estimated_cost leads to the comparison. fn gt_integer() { let a = NodeCost { - index: ArrayIndex::new(0, 0), + index: ArrayIndex::new_ij(0, 0), cost: 3, estimated_cost: 10, }; let b = NodeCost { - index: ArrayIndex::new(1, 1), + index: ArrayIndex::new_ij(1, 1), cost: 5, estimated_cost: 7, }; @@ -163,12 +163,12 @@ mod test { #[test] fn cost_gt_integer() { let a = NodeCost { - index: ArrayIndex::new(0, 0), + index: ArrayIndex::new_ij(0, 0), cost: 5, estimated_cost: 10, }; let b = NodeCost { - index: ArrayIndex::new(1, 1), + index: ArrayIndex::new_ij(1, 1), cost: 3, estimated_cost: 10, }; @@ -178,12 +178,12 @@ mod test { #[test] fn estimated_gt_integer() { let a = NodeCost { - index: ArrayIndex::new(0, 0), + index: ArrayIndex::new_ij(0, 0), cost: 3, estimated_cost: 10, }; let b = NodeCost { - index: ArrayIndex::new(1, 1), + index: ArrayIndex::new_ij(1, 1), cost: 3, estimated_cost: 7, }; @@ -193,12 +193,12 @@ mod test { #[test] fn equal_floats() { let a = NodeCost { - index: ArrayIndex::new(0, 0), + index: ArrayIndex::new_ij(0, 0), cost: 5.0, estimated_cost: 10.0, }; let b = NodeCost { - index: ArrayIndex::new(1, 1), + index: ArrayIndex::new_ij(1, 1), cost: 5.0, estimated_cost: 10.0, }; @@ -208,12 +208,12 @@ mod test { #[test] fn gt_float() { let a = NodeCost { - index: ArrayIndex::new(0, 0), + index: ArrayIndex::new_ij(0, 0), cost: 3.0, estimated_cost: 10.0, }; let b = NodeCost { - index: ArrayIndex::new(1, 1), + index: ArrayIndex::new_ij(1, 1), cost: 5.0, estimated_cost: 7.0, }; @@ -223,12 +223,12 @@ mod test { #[test] fn cost_gt_float() { let a = NodeCost { - index: ArrayIndex::new(0, 0), + index: ArrayIndex::new_ij(0, 0), cost: 5.0, estimated_cost: 10.0, }; let b = NodeCost { - index: ArrayIndex::new(1, 1), + index: ArrayIndex::new_ij(1, 1), cost: 3.0, estimated_cost: 10.0, }; @@ -239,57 +239,57 @@ mod test { fn binary_heap_integer() { let mut heap = std::collections::BinaryHeap::new(); heap.push(NodeCost { - index: ArrayIndex::new(0, 0), + index: ArrayIndex::new_ij(0, 0), cost: 5, estimated_cost: 10, }); heap.push(NodeCost { - index: ArrayIndex::new(1, 0), + index: ArrayIndex::new_ij(1, 0), cost: 3, estimated_cost: 10, }); heap.push(NodeCost { - index: ArrayIndex::new(0, 1), + index: ArrayIndex::new_ij(0, 1), cost: 3, estimated_cost: 12, }); - assert_eq!(heap.pop().unwrap().index, ArrayIndex::new(1, 0)); - assert_eq!(heap.pop().unwrap().index, ArrayIndex::new(0, 0)); - assert_eq!(heap.pop().unwrap().index, ArrayIndex::new(0, 1)); + assert_eq!(heap.pop().unwrap().index, ArrayIndex::new_ij(1, 0)); + assert_eq!(heap.pop().unwrap().index, ArrayIndex::new_ij(0, 0)); + assert_eq!(heap.pop().unwrap().index, ArrayIndex::new_ij(0, 1)); } #[test] fn binary_heap_float() { let mut heap = std::collections::BinaryHeap::new(); heap.push(NodeCost { - index: ArrayIndex::new(0, 0), + index: ArrayIndex::new_ij(0, 0), cost: 5.0, estimated_cost: 10.0, }); heap.push(NodeCost { - index: ArrayIndex::new(1, 0), + index: ArrayIndex::new_ij(1, 0), cost: 3.0, estimated_cost: 10.0, }); heap.push(NodeCost { - index: ArrayIndex::new(0, 1), + index: ArrayIndex::new_ij(0, 1), cost: 3.0, estimated_cost: 12.0, }); - assert_eq!(heap.pop().unwrap().index, ArrayIndex::new(1, 0)); - assert_eq!(heap.pop().unwrap().index, ArrayIndex::new(0, 0)); - assert_eq!(heap.pop().unwrap().index, ArrayIndex::new(0, 1)); + assert_eq!(heap.pop().unwrap().index, ArrayIndex::new_ij(1, 0)); + assert_eq!(heap.pop().unwrap().index, ArrayIndex::new_ij(0, 0)); + assert_eq!(heap.pop().unwrap().index, ArrayIndex::new_ij(0, 1)); } #[test] fn order_u64() { let a = NodeCost { - index: ArrayIndex::new(0, 0), + index: ArrayIndex::new_ij(0, 0), cost: 5_u64, estimated_cost: 10_u64, }; let b = NodeCost { - index: ArrayIndex::new(1, 1), + index: ArrayIndex::new_ij(1, 1), cost: 3_u64, estimated_cost: 10_u64, }; @@ -301,12 +301,12 @@ mod test { #[test] fn order_i32() { let a = NodeCost { - index: ArrayIndex::new(0, 0), + index: ArrayIndex::new_ij(0, 0), cost: 3_i32, estimated_cost: 10_i32, }; let b = NodeCost { - index: ArrayIndex::new(1, 1), + index: ArrayIndex::new_ij(1, 1), cost: 5_i32, estimated_cost: 7_i32, }; @@ -318,12 +318,12 @@ mod test { #[test] fn order_i64() { let a = NodeCost { - index: ArrayIndex::new(0, 0), + index: ArrayIndex::new_ij(0, 0), cost: 5_i64, estimated_cost: 10_i64, }; let b = NodeCost { - index: ArrayIndex::new(1, 1), + index: ArrayIndex::new_ij(1, 1), cost: 3_i64, estimated_cost: 10_i64, }; @@ -336,71 +336,71 @@ mod test { fn binary_heap_u64() { let mut heap = std::collections::BinaryHeap::new(); heap.push(NodeCost { - index: ArrayIndex::new(0, 0), + index: ArrayIndex::new_ij(0, 0), cost: 5_u64, estimated_cost: 10_u64, }); heap.push(NodeCost { - index: ArrayIndex::new(1, 0), + index: ArrayIndex::new_ij(1, 0), cost: 3_u64, estimated_cost: 10_u64, }); heap.push(NodeCost { - index: ArrayIndex::new(0, 1), + index: ArrayIndex::new_ij(0, 1), cost: 3_u64, estimated_cost: 12_u64, }); - assert_eq!(heap.pop().unwrap().index, ArrayIndex::new(1, 0)); - assert_eq!(heap.pop().unwrap().index, ArrayIndex::new(0, 0)); - assert_eq!(heap.pop().unwrap().index, ArrayIndex::new(0, 1)); + assert_eq!(heap.pop().unwrap().index, ArrayIndex::new_ij(1, 0)); + assert_eq!(heap.pop().unwrap().index, ArrayIndex::new_ij(0, 0)); + assert_eq!(heap.pop().unwrap().index, ArrayIndex::new_ij(0, 1)); } #[test] fn binary_heap_i32() { let mut heap = std::collections::BinaryHeap::new(); heap.push(NodeCost { - index: ArrayIndex::new(0, 0), + index: ArrayIndex::new_ij(0, 0), cost: 5_i32, estimated_cost: 10_i32, }); heap.push(NodeCost { - index: ArrayIndex::new(1, 0), + index: ArrayIndex::new_ij(1, 0), cost: 3_i32, estimated_cost: 10_i32, }); heap.push(NodeCost { - index: ArrayIndex::new(0, 1), + index: ArrayIndex::new_ij(0, 1), cost: 3_i32, estimated_cost: 12_i32, }); - assert_eq!(heap.pop().unwrap().index, ArrayIndex::new(1, 0)); - assert_eq!(heap.pop().unwrap().index, ArrayIndex::new(0, 0)); - assert_eq!(heap.pop().unwrap().index, ArrayIndex::new(0, 1)); + assert_eq!(heap.pop().unwrap().index, ArrayIndex::new_ij(1, 0)); + assert_eq!(heap.pop().unwrap().index, ArrayIndex::new_ij(0, 0)); + assert_eq!(heap.pop().unwrap().index, ArrayIndex::new_ij(0, 1)); } #[test] fn binary_heap_i64() { let mut heap = std::collections::BinaryHeap::new(); heap.push(NodeCost { - index: ArrayIndex::new(0, 0), + index: ArrayIndex::new_ij(0, 0), cost: 5_i64, estimated_cost: 10_i64, }); heap.push(NodeCost { - index: ArrayIndex::new(1, 0), + index: ArrayIndex::new_ij(1, 0), cost: 3_i64, estimated_cost: 10_i64, }); heap.push(NodeCost { - index: ArrayIndex::new(0, 1), + index: ArrayIndex::new_ij(0, 1), cost: 3_i64, estimated_cost: 12_i64, }); - assert_eq!(heap.pop().unwrap().index, ArrayIndex::new(1, 0)); - assert_eq!(heap.pop().unwrap().index, ArrayIndex::new(0, 0)); - assert_eq!(heap.pop().unwrap().index, ArrayIndex::new(0, 1)); + assert_eq!(heap.pop().unwrap().index, ArrayIndex::new_ij(1, 0)); + assert_eq!(heap.pop().unwrap().index, ArrayIndex::new_ij(0, 0)); + assert_eq!(heap.pop().unwrap().index, ArrayIndex::new_ij(0, 1)); } } diff --git a/crates/revrt/src/network/long_range/mod.rs b/crates/revrt/src/network/long_range/mod.rs index e8449d74..616d0382 100644 --- a/crates/revrt/src/network/long_range/mod.rs +++ b/crates/revrt/src/network/long_range/mod.rs @@ -503,9 +503,9 @@ mod tests { #[test] fn state_spills_when_pressure_exceeds_budget() { - let start = ArrayIndex::new(10, 10); - let goal = ArrayIndex::new(15, 15); - let mut state = FrontierOnlySearchState::new(&start, 2_000, (31, 31)).unwrap(); + let start = ArrayIndex::new_ij(10, 10); + let goal = ArrayIndex::new_ij(15, 15); + let mut state = FrontierOnlySearchState::new(&start, 2_000, (31, 31, 1)).unwrap(); let ans = run_to_goal(&mut state, goal.clone(), |p| { let mut out = Vec::new(); @@ -517,7 +517,7 @@ mod tests { let ni = p.i as i64 + di; let nj = p.j as i64 + dj; if ni >= 0 && nj >= 0 && ni <= 30 && nj <= 30 { - out.push((ArrayIndex::new(ni as u64, nj as u64), 1_u64)); + out.push((ArrayIndex::new_ij(ni as u64, nj as u64), 1_u64)); } } } @@ -531,16 +531,16 @@ mod tests { #[test] fn state_returns_none_when_frontier_never_reaches_goal() { - let start = ArrayIndex::new(0, 0); - let mut state = FrontierOnlySearchState::new(&start, 2_000, (21, 21)).unwrap(); + let start = ArrayIndex::new_ij(0, 0); + let mut state = FrontierOnlySearchState::new(&start, 2_000, (21, 21, 1)).unwrap(); - let ans = run_to_goal(&mut state, ArrayIndex::new(99, 99), |p| { + let ans = run_to_goal(&mut state, ArrayIndex::new_ij(99, 99), |p| { let mut out = Vec::new(); if p.i < 20 { - out.push((ArrayIndex::new(p.i + 1, p.j), 1_u64)); + out.push((ArrayIndex::new_ij(p.i + 1, p.j), 1_u64)); } if p.j < 20 { - out.push((ArrayIndex::new(p.i, p.j + 1), 1_u64)); + out.push((ArrayIndex::new_ij(p.i, p.j + 1), 1_u64)); } out }); @@ -551,45 +551,48 @@ mod tests { #[test] fn multi_source_state_skips_invalid_roots() { let starts = vec![ - ArrayIndex::new(0, 0), - ArrayIndex::new(0, 0), - ArrayIndex::new(999, 999), + ArrayIndex::new_ij(0, 0), + ArrayIndex::new_ij(0, 0), + ArrayIndex::new_ij(999, 999), ]; - let mut state = FrontierOnlySearchState::new_many(&starts, 2_000, (3, 3)).unwrap(); + let mut state = FrontierOnlySearchState::new_many(&starts, 2_000, (3, 3, 1)).unwrap(); let node = state.pop_next_node().unwrap(); - assert_eq!(node.array_index, ArrayIndex::new(0, 0)); + assert_eq!(node.array_index, ArrayIndex::new_ij(0, 0)); assert!(state.pop_next_node().is_none()); } #[test] fn reconstruct_path_works_for_frontier_nodes() { - let start = ArrayIndex::new(0, 0); - let mut state = FrontierOnlySearchState::new(&start, 2_000, (3, 3)).unwrap(); + let start = ArrayIndex::new_ij(0, 0); + let mut state = FrontierOnlySearchState::new(&start, 2_000, (3, 3, 1)).unwrap(); let node = state.pop_next_node().unwrap(); state - .add_successors(&node, vec![(ArrayIndex::new(0, 1), 1_u64)]) + .add_successors(&node, vec![(ArrayIndex::new_ij(0, 1), 1_u64)]) .unwrap(); - let slot = state.grid.slot_of(&ArrayIndex::new(0, 1)).unwrap(); + let slot = state.grid.slot_of(&ArrayIndex::new_ij(0, 1)).unwrap(); let path = state.reconstruct_path_to(slot).unwrap(); - assert_eq!(path, vec![ArrayIndex::new(0, 0), ArrayIndex::new(0, 1)]); + assert_eq!( + path, + vec![ArrayIndex::new_ij(0, 0), ArrayIndex::new_ij(0, 1)] + ); } #[test] fn frontier_prefers_lower_estimated_cost() { - let start = ArrayIndex::new(0, 0); - let mut state = FrontierOnlySearchState::new(&start, 2_000, (3, 3)).unwrap(); + let start = ArrayIndex::new_ij(0, 0); + let mut state = FrontierOnlySearchState::new(&start, 2_000, (3, 3, 1)).unwrap(); let node = state.pop_next_node().unwrap(); state .add_successors_tracking_with_estimator( &node, vec![ - (ArrayIndex::new(0, 1), 1_u64), - (ArrayIndex::new(1, 0), 2_u64), + (ArrayIndex::new_ij(0, 1), 1_u64), + (ArrayIndex::new_ij(1, 0), 2_u64), ], |neighbor, cost| match (neighbor.i, neighbor.j) { (0, 1) => cost.saturating_add(20), @@ -602,30 +605,30 @@ mod tests { let next = state.pop_next_node().unwrap(); - assert_eq!(next.array_index, ArrayIndex::new(1, 0)); + assert_eq!(next.array_index, ArrayIndex::new_ij(1, 0)); assert_eq!(next.cost, 2); } #[test] fn bidirectional_search_merges_forward_and_backward_paths() { - let start = ArrayIndex::new(0, 0); - let goals = vec![ArrayIndex::new(2, 2)]; - let mut state = BidirectionalSearchState::new(&start, &goals, 2_000, (3, 3)).unwrap(); + let start = ArrayIndex::new_ij(0, 0); + let goals = vec![ArrayIndex::new_ij(2, 2)]; + let mut state = BidirectionalSearchState::new(&start, &goals, 2_000, (3, 3, 1)).unwrap(); let (route, cost) = state .run(|p| { let mut out = Vec::new(); if p.i > 0 { - out.push((ArrayIndex::new(p.i - 1, p.j), 1_u64)); + out.push((ArrayIndex::new_ij(p.i - 1, p.j), 1_u64)); } if p.i < 2 { - out.push((ArrayIndex::new(p.i + 1, p.j), 1_u64)); + out.push((ArrayIndex::new_ij(p.i + 1, p.j), 1_u64)); } if p.j > 0 { - out.push((ArrayIndex::new(p.i, p.j - 1), 1_u64)); + out.push((ArrayIndex::new_ij(p.i, p.j - 1), 1_u64)); } if p.j < 2 { - out.push((ArrayIndex::new(p.i, p.j + 1), 1_u64)); + out.push((ArrayIndex::new_ij(p.i, p.j + 1), 1_u64)); } out }) @@ -633,6 +636,31 @@ mod tests { assert_eq!(cost, 4); assert_eq!(route.first(), Some(&start)); - assert_eq!(route.last(), Some(&ArrayIndex::new(2, 2))); + assert_eq!(route.last(), Some(&ArrayIndex::new_ij(2, 2))); + } + + #[test] + fn state_supports_nonzero_option_indices() { + let start = ArrayIndex { + i: 0, + j: 0, + option: 1, + }; + let goal = ArrayIndex { + i: 0, + j: 1, + option: 1, + }; + let mut state = FrontierOnlySearchState::new(&start, 2_000, (2, 2, 2)).unwrap(); + let node = state.pop_next_node().unwrap(); + + state + .add_successors(&node, vec![(goal.clone(), 1_u64)]) + .unwrap(); + + let slot = state.grid.slot_of(&goal).unwrap(); + let path = state.reconstruct_path_to(slot).unwrap(); + + assert_eq!(path, vec![start, goal]); } } diff --git a/crates/revrt/src/network/long_range/utilities.rs b/crates/revrt/src/network/long_range/utilities.rs index b87fa75f..2fd66905 100644 --- a/crates/revrt/src/network/long_range/utilities.rs +++ b/crates/revrt/src/network/long_range/utilities.rs @@ -32,8 +32,9 @@ impl GridIndexer { pub(super) fn index_of(&self, slot: usize) -> ArrayIndex { let linear = slot as u64; ArrayIndex { - i: linear / self.ncols, - j: linear % self.ncols, + i: planar / self.ncols, + j: planar % self.ncols, + option, } } @@ -82,10 +83,10 @@ mod tests { fn grid_indexer_round_trip() { let grid = GridIndexer::new(7, 9).unwrap(); let sample = [ - ArrayIndex::new(0, 0), - ArrayIndex::new(1, 4), - ArrayIndex::new(3, 8), - ArrayIndex::new(6, 7), + ArrayIndex::new_ij(0, 0), + ArrayIndex::new_ij(1, 4), + ArrayIndex::new_ij(3, 8), + ArrayIndex::new_ij(6, 7), ]; for index in sample { @@ -106,11 +107,11 @@ mod tests { fn grid_indexer_slot_of_rejects_out_of_bounds_indices() { let grid = GridIndexer::new(3, 4).unwrap(); - assert_eq!(grid.slot_of(&ArrayIndex::new(0, 0)), Some(0)); - assert_eq!(grid.slot_of(&ArrayIndex::new(2, 3)), Some(11)); - assert_eq!(grid.slot_of(&ArrayIndex::new(3, 0)), None); - assert_eq!(grid.slot_of(&ArrayIndex::new(0, 4)), None); - assert_eq!(grid.slot_of(&ArrayIndex::new(9, 9)), None); + assert_eq!(grid.slot_of(&ArrayIndex::new_ij(0, 0)), Some(0)); + assert_eq!(grid.slot_of(&ArrayIndex::new_ij(2, 3)), Some(11)); + assert_eq!(grid.slot_of(&ArrayIndex::new_ij(3, 0)), None); + assert_eq!(grid.slot_of(&ArrayIndex::new_ij(0, 4)), None); + assert_eq!(grid.slot_of(&ArrayIndex::new_ij(9, 9)), None); } #[test] diff --git a/crates/revrt/src/network/unused.rs b/crates/revrt/src/network/unused.rs index b3943473..da063ba7 100644 --- a/crates/revrt/src/network/unused.rs +++ b/crates/revrt/src/network/unused.rs @@ -130,54 +130,66 @@ mod tests { /// Adding the first node to an empty network fn first_node() { let mut network = Network::new(); - let id = network.add_node(ArrayIndex::new(10, 10), 0, None); + let id = network.add_node(ArrayIndex::new_ij(10, 10), 0, None); assert_eq!(id, 0); - assert!(network.nodes.contains_key(&ArrayIndex::new(10, 10))); + assert!(network.nodes.contains_key(&ArrayIndex::new_ij(10, 10))); let next = network.pop().unwrap(); - assert_eq!(next.position, ArrayIndex::new(10, 10)); + assert_eq!(next.position, ArrayIndex::new_ij(10, 10)); } #[test] fn test_add_sequence_of_nodes() { let mut network = Network::new(); // First node - let id1 = network.add_node(ArrayIndex::new(10, 10), 0, None); + let id1 = network.add_node(ArrayIndex::new_ij(10, 10), 0, None); // Next node - let id2 = network.add_node(ArrayIndex::new(9, 9), 3, Some(ArrayIndex::new(10, 10))); + let id2 = network.add_node( + ArrayIndex::new_ij(9, 9), + 3, + Some(ArrayIndex::new_ij(10, 10)), + ); assert_eq!(id1, 0); assert_eq!(id2, 1); - assert!(network.nodes.contains_key(&ArrayIndex::new(10, 10))); - assert!(network.nodes.contains_key(&ArrayIndex::new(9, 9))); + assert!(network.nodes.contains_key(&ArrayIndex::new_ij(10, 10))); + assert!(network.nodes.contains_key(&ArrayIndex::new_ij(9, 9))); } #[test] fn retrieve_cheapest_node() { let mut network = Network::new(); - network.add_node(ArrayIndex::new(10, 10), 100, None); - network.add_node(ArrayIndex::new(9, 9), 50, None); - network.add_node(ArrayIndex::new(8, 8), 75, None); + network.add_node(ArrayIndex::new_ij(10, 10), 100, None); + network.add_node(ArrayIndex::new_ij(9, 9), 50, None); + network.add_node(ArrayIndex::new_ij(8, 8), 75, None); let next = network.pop().unwrap(); - assert_eq!(next.position, ArrayIndex::new(9, 9)); + assert_eq!(next.position, ArrayIndex::new_ij(9, 9)); assert_eq!(next.cost, 50); } #[test] fn multiple_edges_to_node() { let mut network = Network::new(); - network.add_node(ArrayIndex::new(10, 10), 10, None); - network.add_node(ArrayIndex::new(11, 11), 11, None); - network.add_node(ArrayIndex::new(9, 9), 13, Some(ArrayIndex::new(10, 10))); - network.add_node(ArrayIndex::new(9, 9), 12, Some(ArrayIndex::new(11, 11))); + network.add_node(ArrayIndex::new_ij(10, 10), 10, None); + network.add_node(ArrayIndex::new_ij(11, 11), 11, None); + network.add_node( + ArrayIndex::new_ij(9, 9), + 13, + Some(ArrayIndex::new_ij(10, 10)), + ); + network.add_node( + ArrayIndex::new_ij(9, 9), + 12, + Some(ArrayIndex::new_ij(11, 11)), + ); assert_eq!(network.nodes.len(), 3); - assert!(network.nodes.contains_key(&ArrayIndex::new(9, 9))); - let node = network.nodes.get(&ArrayIndex::new(9, 9)).unwrap(); + assert!(network.nodes.contains_key(&ArrayIndex::new_ij(9, 9))); + let node = network.nodes.get(&ArrayIndex::new_ij(9, 9)).unwrap(); assert_eq!(node.edges.len(), 2); - assert!(node.edges.contains_key(&ArrayIndex::new(10, 10))); - assert_eq!(node.edges[&ArrayIndex::new(10, 10)].cost, 13); - assert!(node.edges.contains_key(&ArrayIndex::new(11, 11))); - assert_eq!(node.edges[&ArrayIndex::new(11, 11)].cost, 12); + assert!(node.edges.contains_key(&ArrayIndex::new_ij(10, 10))); + assert_eq!(node.edges[&ArrayIndex::new_ij(10, 10)].cost, 13); + assert!(node.edges.contains_key(&ArrayIndex::new_ij(11, 11))); + assert_eq!(node.edges[&ArrayIndex::new_ij(11, 11)].cost, 12); } } diff --git a/crates/revrt/src/routing/astar.rs b/crates/revrt/src/routing/astar.rs index 41242e39..8bdd259c 100644 --- a/crates/revrt/src/routing/astar.rs +++ b/crates/revrt/src/routing/astar.rs @@ -86,8 +86,8 @@ mod tests { #[test] fn octile_distance_uses_shortest_goal() { - let goals = [ArrayIndex::new(3, 3), ArrayIndex::new(8, 8)]; - let distance = super::octile_distance(&ArrayIndex::new(0, 1), &goals); + let goals = [ArrayIndex::new_ij(3, 3), ArrayIndex::new_ij(8, 8)]; + let distance = super::octile_distance(&ArrayIndex::new_ij(0, 1), &goals); assert_eq!(distance, 1.0 + 2.0 * std::f64::consts::SQRT_2); } @@ -95,9 +95,10 @@ mod tests { #[test] fn octile_heuristic_defaults_to_one_when_min_cost_unknown() { let min_cost = Cell::new(None); - let goals = [ArrayIndex::new(3, 3)]; + let goals = [ArrayIndex::new_ij(3, 3)]; - let heuristic = super::octile_heuristic::(&ArrayIndex::new(0, 1), &goals, &min_cost); + let heuristic = + super::octile_heuristic::(&ArrayIndex::new_ij(0, 1), &goals, &min_cost); assert_eq!(heuristic, 3); } @@ -107,8 +108,13 @@ mod tests { let min_cost = Cell::new(Some(70)); let neighbors = super::astar_successors( - &ArrayIndex::new(1, 1), - &mut |_| vec![(ArrayIndex::new(1, 2), 15_u64), (ArrayIndex::new(2, 2), 12)], + &ArrayIndex::new_ij(1, 1), + &mut |_| { + vec![ + (ArrayIndex::new_ij(1, 2), 15_u64), + (ArrayIndex::new_ij(2, 2), 12), + ] + }, &min_cost, ); diff --git a/crates/revrt/src/routing/long_range.rs b/crates/revrt/src/routing/long_range.rs index 5d9fc7a9..48bcf3bf 100644 --- a/crates/revrt/src/routing/long_range.rs +++ b/crates/revrt/src/routing/long_range.rs @@ -147,8 +147,8 @@ mod tests { #[test] fn bounded_astar_finds_shortest_path() { - let start = ArrayIndex::new(0, 0); - let goal = ArrayIndex::new(2, 2); + let start = ArrayIndex::new_ij(0, 0); + let goal = ArrayIndex::new_ij(2, 2); let ans = long_range_astar( &start, @@ -156,16 +156,16 @@ mod tests { |p: &ArrayIndex| { let mut out = Vec::new(); if p.i < 2 { - out.push((ArrayIndex::new(p.i + 1, p.j), 1_u64)); + out.push((ArrayIndex::new_ij(p.i + 1, p.j), 1_u64)); } if p.j < 2 { - out.push((ArrayIndex::new(p.i, p.j + 1), 1_u64)); + out.push((ArrayIndex::new_ij(p.i, p.j + 1), 1_u64)); } out }, |p| *p == goal, 2 * 1024 * 1024, - (3, 3), + (3, 3, 1), ) .unwrap(); @@ -176,24 +176,24 @@ mod tests { #[test] fn bounded_finds_shortest_path() { - let start = ArrayIndex::new(0, 0); - let goal = ArrayIndex::new(2, 2); + let start = ArrayIndex::new_ij(0, 0); + let goal = ArrayIndex::new_ij(2, 2); let ans = long_range_dijkstra( &start, |p: &ArrayIndex| { let mut out = Vec::new(); if p.i < 2 { - out.push((ArrayIndex::new(p.i + 1, p.j), 1_u64)); + out.push((ArrayIndex::new_ij(p.i + 1, p.j), 1_u64)); } if p.j < 2 { - out.push((ArrayIndex::new(p.i, p.j + 1), 1_u64)); + out.push((ArrayIndex::new_ij(p.i, p.j + 1), 1_u64)); } out }, |p| *p == goal, 2 * 1024 * 1024 * 1024, - (3, 3), + (3, 3, 1), ) .unwrap(); @@ -204,7 +204,7 @@ mod tests { #[test] fn bounded_astar_rejects_missing_goals() { - let start = ArrayIndex::new(0, 0); + let start = ArrayIndex::new_ij(0, 0); let ans = long_range_astar( &start, @@ -212,7 +212,7 @@ mod tests { |_p: &ArrayIndex| Vec::<(ArrayIndex, u64)>::new(), |_p| false, 2 * 1024 * 1024, - (1, 1), + (1, 1, 1), ); assert!(ans.is_none()); @@ -220,8 +220,8 @@ mod tests { #[test] fn bounded_astar_returns_zero_cost_when_start_is_goal() { - let start = ArrayIndex::new(0, 0); - let goals = vec![start.clone(), ArrayIndex::new(1, 1)]; + let start = ArrayIndex::new_ij(0, 0); + let goals = vec![start.clone(), ArrayIndex::new_ij(1, 1)]; let ans = long_range_astar( &start, @@ -229,7 +229,7 @@ mod tests { |_p: &ArrayIndex| Vec::<(ArrayIndex, u64)>::new(), |_p| false, 2 * 1024 * 1024, - (2, 2), + (2, 2, 1), ) .unwrap(); @@ -239,14 +239,14 @@ mod tests { #[test] fn bounded_rejects_too_small_budget() { - let start = ArrayIndex::new(0, 0); + let start = ArrayIndex::new_ij(0, 0); let ans = long_range_dijkstra( &start, |_p: &ArrayIndex| Vec::<(ArrayIndex, u64)>::new(), |_p| false, 1024, - (1, 1), + (1, 1, 1), ); assert!(ans.is_none()); @@ -254,8 +254,8 @@ mod tests { #[test] fn bidirectional_bounded_finds_shortest_path_to_any_goal() { - let start = ArrayIndex::new(0, 0); - let goals = vec![ArrayIndex::new(2, 2), ArrayIndex::new(0, 2)]; + let start = ArrayIndex::new_ij(0, 0); + let goals = vec![ArrayIndex::new_ij(2, 2), ArrayIndex::new_ij(0, 2)]; let ans = bidirectional_long_range_dijkstra( &start, @@ -263,33 +263,33 @@ mod tests { |p: &ArrayIndex| { let mut out = Vec::new(); if p.i < 2 { - out.push((ArrayIndex::new(p.i + 1, p.j), 1_u64)); + out.push((ArrayIndex::new_ij(p.i + 1, p.j), 1_u64)); } if p.j < 2 { - out.push((ArrayIndex::new(p.i, p.j + 1), 1_u64)); + out.push((ArrayIndex::new_ij(p.i, p.j + 1), 1_u64)); } out }, 2 * 1024 * 1024, - (3, 3), + (3, 3, 1), ) .unwrap(); assert_eq!(ans.1, 2_u64); assert_eq!(ans.0.first(), Some(&start)); - assert_eq!(ans.0.last(), Some(&ArrayIndex::new(0, 2))); + assert_eq!(ans.0.last(), Some(&ArrayIndex::new_ij(0, 2))); } #[test] fn bidirectional_bounded_rejects_missing_goals() { - let start = ArrayIndex::new(0, 0); + let start = ArrayIndex::new_ij(0, 0); let ans = bidirectional_long_range_dijkstra( &start, &[], |_p: &ArrayIndex| Vec::<(ArrayIndex, u64)>::new(), 2 * 1024 * 1024, - (1, 1), + (1, 1, 1), ); assert!(ans.is_none()); diff --git a/crates/revrt/src/routing/mod.rs b/crates/revrt/src/routing/mod.rs index 57cabcb6..53b3169f 100644 --- a/crates/revrt/src/routing/mod.rs +++ b/crates/revrt/src/routing/mod.rs @@ -306,8 +306,8 @@ mod tests { .unwrap(); let scenario = Scenario::new(store.path(), cost_function, 1_000).unwrap(); let algorithm = Algorithm::from_selection(AlgorithmType::Dijkstra, 8 * 1024 * 1024); - let start = [ArrayIndex::new(0, 0), ArrayIndex::new(2, 0)]; - let end = [ArrayIndex::new(0, 4), ArrayIndex::new(2, 4)]; + let start = [ArrayIndex::new_ij(0, 0), ArrayIndex::new_ij(2, 0)]; + let end = [ArrayIndex::new_ij(0, 4), ArrayIndex::new_ij(2, 4)]; let result = compute_route_attempt_result(&scenario, &algorithm, &start, &end); @@ -315,9 +315,9 @@ mod tests { let top_solution = result .iter() - .find(|solution| solution.route().first() == Some(&ArrayIndex::new(0, 0))) + .find(|solution| solution.route().first() == Some(&ArrayIndex::new_ij(0, 0))) .unwrap(); - assert_eq!(top_solution.route().last(), Some(&ArrayIndex::new(0, 4))); + assert_eq!(top_solution.route().last(), Some(&ArrayIndex::new_ij(0, 4))); assert_eq!( top_solution.dropped_barrier_layers(), &vec!["soft_barrier".to_string()] @@ -325,9 +325,12 @@ mod tests { let bottom_solution = result .iter() - .find(|solution| solution.route().first() == Some(&ArrayIndex::new(2, 0))) + .find(|solution| solution.route().first() == Some(&ArrayIndex::new_ij(2, 0))) .unwrap(); - assert_eq!(bottom_solution.route().last(), Some(&ArrayIndex::new(2, 4))); + assert_eq!( + bottom_solution.route().last(), + Some(&ArrayIndex::new_ij(2, 4)) + ); assert!(bottom_solution.dropped_barrier_layers().is_empty()); } } diff --git a/crates/revrt/src/routing/scenario.rs b/crates/revrt/src/routing/scenario.rs index 2f4ffc19..196db1b8 100644 --- a/crates/revrt/src/routing/scenario.rs +++ b/crates/revrt/src/routing/scenario.rs @@ -325,7 +325,7 @@ mod tests { .unwrap(); let scenario = Scenario::new(store.path(), cost_function, 1_000).unwrap(); - let start = ArrayIndex { i: 1, j: 1 }; + let start = ArrayIndex::new_ij(1, 1); let initial_successors = scenario.successors_for_attempt(&start, 0); let retry_one_successors = scenario.successors_for_attempt(&start, 1); let retry_two_successors = scenario.successors_for_attempt(&start, 2); @@ -334,32 +334,32 @@ mod tests { assert!( !initial_successors .iter() - .any(|(p, _)| *p == ArrayIndex { i: 1, j: 0 }) + .any(|(p, _)| *p == ArrayIndex::new_ij(1, 0)) ); assert!( !initial_successors .iter() - .any(|(p, _)| *p == ArrayIndex { i: 0, j: 1 }) + .any(|(p, _)| *p == ArrayIndex::new_ij(0, 1)) ); assert!( retry_one_successors .iter() - .any(|(p, _)| *p == ArrayIndex { i: 1, j: 0 }) + .any(|(p, _)| *p == ArrayIndex::new_ij(1, 0)) ); assert!( !retry_one_successors .iter() - .any(|(p, _)| *p == ArrayIndex { i: 0, j: 1 }) + .any(|(p, _)| *p == ArrayIndex::new_ij(0, 1)) ); assert!( retry_two_successors .iter() - .any(|(p, _)| *p == ArrayIndex { i: 1, j: 0 }) + .any(|(p, _)| *p == ArrayIndex::new_ij(1, 0)) ); assert!( retry_two_successors .iter() - .any(|(p, _)| *p == ArrayIndex { i: 0, j: 1 }) + .any(|(p, _)| *p == ArrayIndex::new_ij(0, 1)) ); } @@ -396,12 +396,12 @@ mod tests { assert!( scenario - .successors_for_attempt(&ArrayIndex { i: 1, j: 1 }, 0) + .successors_for_attempt(&ArrayIndex::new_ij(1, 1), 0) .is_empty() ); assert!( !scenario - .successors_for_attempt(&ArrayIndex { i: 1, j: 1 }, 1) + .successors_for_attempt(&ArrayIndex::new_ij(1, 1), 1) .is_empty() ); } diff --git a/tests/rust/integration_tests.rs b/tests/rust/integration_tests.rs index 8511d8ef..1a856d85 100644 --- a/tests/rust/integration_tests.rs +++ b/tests/rust/integration_tests.rs @@ -13,8 +13,8 @@ const TEST_DATA: &str = concat!( #[test_case("bidirectional-long-range-dijkstra"; "bidirectional-long-range")] fn basic_routing_in_data(algorithm: &str) { let layers_path = PathBuf::from(TEST_DATA); - let start = &revrt::ArrayIndex::new(10, 10); - let end = vec![revrt::ArrayIndex::new(20, 20)]; + let start = &revrt::ArrayIndex::new_ij(10, 10); + let end = vec![revrt::ArrayIndex::new_ij(20, 20)]; let result = resolve( layers_path.to_str().expect("test data path is valid UTF-8"), r#"{"cost_layers": [{"layer_name": "tie_line_costs_102MW"}]}"#, @@ -37,8 +37,8 @@ fn basic_routing_in_data(algorithm: &str) { #[test_case("bidirectional-long-range-dijkstra"; "bidirectional-long-range")] fn basic_routing_in_data_with_friction(algorithm: &str) { let layers_path = PathBuf::from(TEST_DATA); - let start = &revrt::ArrayIndex::new(10, 10); - let end = vec![revrt::ArrayIndex::new(20, 20)]; + let start = &revrt::ArrayIndex::new_ij(10, 10); + let end = vec![revrt::ArrayIndex::new_ij(20, 20)]; let result = resolve( layers_path.to_str().expect("test data path is valid UTF-8"), r#"{ From b64191d67ff0c3c6a3c4b13eb6733e379224b809 Mon Sep 17 00:00:00 2001 From: Paul Date: Mon, 11 May 2026 13:33:43 -0600 Subject: [PATCH 02/40] Update grid shape to be 3D --- crates/revrt/src/dataset/mod.rs | 4 ++-- crates/revrt/src/dataset/reader.rs | 7 ++++--- crates/revrt/src/network/long_range/mod.rs | 6 +++--- crates/revrt/src/routing/algorithm.rs | 2 +- crates/revrt/src/routing/long_range.rs | 6 +++--- crates/revrt/src/routing/scenario.rs | 4 ++-- 6 files changed, 15 insertions(+), 14 deletions(-) diff --git a/crates/revrt/src/dataset/mod.rs b/crates/revrt/src/dataset/mod.rs index 9d1d0d0c..7b8c4159 100644 --- a/crates/revrt/src/dataset/mod.rs +++ b/crates/revrt/src/dataset/mod.rs @@ -54,8 +54,8 @@ pub(super) struct Dataset { derived_data_writer: DerivedDataWriter, /// Reader responsible for cached neighborhood access to derived data. neighborhood_reader: NeighborhoodReader, - /// Shape of the source routing grid as `(rows, cols)`. - pub(super) grid_shape: (u64, u64), + /// Shape of the source routing grid as `(rows, cols, options)`. + pub(super) grid_shape: (u64, u64, u32), } impl Dataset { diff --git a/crates/revrt/src/dataset/reader.rs b/crates/revrt/src/dataset/reader.rs index e9f78087..d7aba75d 100644 --- a/crates/revrt/src/dataset/reader.rs +++ b/crates/revrt/src/dataset/reader.rs @@ -278,8 +278,8 @@ impl NeighborhoodReader { /// /// # Returns /// The routing grid dimensions recorded when the reader was opened. - pub(super) fn grid_shape(&self) -> (u64, u64) { - (self.grid_nrows, self.grid_ncols) + pub(super) fn grid_shape(&self) -> (u64, u64, u32) { + (self.grid_nrows, self.grid_ncols, self.grid_noptions) } /// Build the row and column ranges for a clipped 3x3 neighborhood. @@ -301,9 +301,10 @@ impl NeighborhoodReader { std::ops::Range, zarrs::array_subset::ArraySubset, ) { - let &ArrayIndex { i, j } = index; + let &ArrayIndex { i, j, option } = index; debug_assert!(self.grid_nrows > 0); debug_assert!(self.grid_ncols > 0); + debug_assert!(option < self.grid_noptions); let max_i = self.grid_nrows - 1; let max_j = self.grid_ncols - 1; diff --git a/crates/revrt/src/network/long_range/mod.rs b/crates/revrt/src/network/long_range/mod.rs index 616d0382..e6bcf3d5 100644 --- a/crates/revrt/src/network/long_range/mod.rs +++ b/crates/revrt/src/network/long_range/mod.rs @@ -54,7 +54,7 @@ impl FrontierOnlySearchState { pub(crate) fn new( start: &ArrayIndex, memory_budget_bytes: u64, - grid_shape: (u64, u64), + grid_shape: (u64, u64, u32), ) -> Option { Self::new_many(std::slice::from_ref(start), memory_budget_bytes, grid_shape) } @@ -62,7 +62,7 @@ impl FrontierOnlySearchState { pub(crate) fn new_many( starts: &[ArrayIndex], memory_budget_bytes: u64, - grid_shape: (u64, u64), + grid_shape: (u64, u64, u32), ) -> Option { let grid = GridIndexer::new(grid_shape.0, grid_shape.1)?; @@ -328,7 +328,7 @@ impl BidirectionalSearchState { start: &ArrayIndex, goals: &[ArrayIndex], memory_budget_bytes: u64, - grid_shape: (u64, u64), + grid_shape: (u64, u64, u32), ) -> Option { let per_direction_budget = (memory_budget_bytes / 2).max(1); diff --git a/crates/revrt/src/routing/algorithm.rs b/crates/revrt/src/routing/algorithm.rs index 9e956cc0..f44043f3 100644 --- a/crates/revrt/src/routing/algorithm.rs +++ b/crates/revrt/src/routing/algorithm.rs @@ -122,7 +122,7 @@ impl Algorithm { goals: &[ArrayIndex], successors: FN, success: FS, - grid_shape: (u64, u64), + grid_shape: (u64, u64, u32), ) -> Option> //) -> Option> where diff --git a/crates/revrt/src/routing/long_range.rs b/crates/revrt/src/routing/long_range.rs index 48bcf3bf..87e215cf 100644 --- a/crates/revrt/src/routing/long_range.rs +++ b/crates/revrt/src/routing/long_range.rs @@ -18,7 +18,7 @@ pub(super) fn long_range_astar( mut successors: FN, mut success: FS, memory_budget_bytes: u64, - grid_shape: (u64, u64), + grid_shape: (u64, u64, u32), ) -> Option<(Vec, C)> where C: Zero + Ord + Copy, @@ -74,7 +74,7 @@ pub(super) fn long_range_dijkstra( mut successors: FN, mut success: FS, memory_budget_bytes: u64, - grid_shape: (u64, u64), + grid_shape: (u64, u64, u32), ) -> Option<(Vec, C)> where C: Zero + Ord + Copy, @@ -112,7 +112,7 @@ pub(super) fn bidirectional_long_range_dijkstra( goals: &[ArrayIndex], successors: FN, memory_budget_bytes: u64, - grid_shape: (u64, u64), + grid_shape: (u64, u64, u32), ) -> Option<(Vec, C)> where C: Zero + Ord + Copy, diff --git a/crates/revrt/src/routing/scenario.rs b/crates/revrt/src/routing/scenario.rs index 196db1b8..047f85e1 100644 --- a/crates/revrt/src/routing/scenario.rs +++ b/crates/revrt/src/routing/scenario.rs @@ -167,8 +167,8 @@ impl Scenario { /// Return the scenario grid dimensions. /// /// # Returns - /// Tuple of `(rows, cols)` describing the routing grid shape. - pub(super) fn grid_shape(&self) -> (u64, u64) { + /// Tuple of `(rows, cols, options)` describing the routing grid shape. + pub(super) fn grid_shape(&self) -> (u64, u64, u32) { self.dataset.grid_shape } } From 72cc16e9b815e7cd804173e31747d8d4a129fe83 Mon Sep 17 00:00:00 2001 From: Paul Date: Mon, 11 May 2026 13:35:21 -0600 Subject: [PATCH 03/40] Array index update --- crates/revrt/src/routing/scenario.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/revrt/src/routing/scenario.rs b/crates/revrt/src/routing/scenario.rs index 047f85e1..e93151a0 100644 --- a/crates/revrt/src/routing/scenario.rs +++ b/crates/revrt/src/routing/scenario.rs @@ -220,30 +220,30 @@ mod tests { .unwrap(); let scenario = Scenario::new(store.path(), cost_function, 1_000).unwrap(); - let start = ArrayIndex { i: 1, j: 1 }; + let start = ArrayIndex::new_ij(1, 1); let initial_successors = scenario.successors_for_attempt(&start, 0); let relaxed_successors = scenario.successors_for_attempt(&start, 1); assert!( !initial_successors .iter() - .any(|(p, _)| *p == ArrayIndex { i: 0, j: 1 }) + .any(|(p, _)| *p == ArrayIndex::new_ij(0, 1)) ); assert!( !initial_successors .iter() - .any(|(p, _)| *p == ArrayIndex { i: 1, j: 0 }) + .any(|(p, _)| *p == ArrayIndex::new_ij(1, 0)) ); assert!( !relaxed_successors .iter() - .any(|(p, _)| *p == ArrayIndex { i: 0, j: 1 }) + .any(|(p, _)| *p == ArrayIndex::new_ij(0, 1)) ); assert!( relaxed_successors .iter() - .any(|(p, _)| *p == ArrayIndex { i: 1, j: 0 }) + .any(|(p, _)| *p == ArrayIndex::new_ij(1, 0)) ); } @@ -277,7 +277,7 @@ mod tests { .unwrap(); let scenario = Scenario::new(store.path(), cost_function, 1_000).unwrap(); - let successors = scenario.successors_for_attempt(&ArrayIndex { i: 1, j: 1 }, 0); + let successors = scenario.successors_for_attempt(&ArrayIndex::new_ij(1, 1), 0); assert!(successors.is_empty()); } From 0f17a6e23f7db0afd83258b0fa1b359443d5bc27 Mon Sep 17 00:00:00 2001 From: Paul Date: Mon, 11 May 2026 17:05:50 -0600 Subject: [PATCH 04/40] New method --- crates/revrt/src/network/long_range/mod.rs | 2 +- .../revrt/src/network/long_range/utilities.rs | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/crates/revrt/src/network/long_range/mod.rs b/crates/revrt/src/network/long_range/mod.rs index e6bcf3d5..674208d1 100644 --- a/crates/revrt/src/network/long_range/mod.rs +++ b/crates/revrt/src/network/long_range/mod.rs @@ -64,7 +64,7 @@ impl FrontierOnlySearchState { memory_budget_bytes: u64, grid_shape: (u64, u64, u32), ) -> Option { - let grid = GridIndexer::new(grid_shape.0, grid_shape.1)?; + let grid = GridIndexer::new(grid_shape.0, grid_shape.1, grid_shape.2)?; let mut state = Self { grid, diff --git a/crates/revrt/src/network/long_range/utilities.rs b/crates/revrt/src/network/long_range/utilities.rs index 2fd66905..ca28c9e0 100644 --- a/crates/revrt/src/network/long_range/utilities.rs +++ b/crates/revrt/src/network/long_range/utilities.rs @@ -81,7 +81,7 @@ mod tests { #[test] fn grid_indexer_round_trip() { - let grid = GridIndexer::new(7, 9).unwrap(); + let grid = GridIndexer::new(7, 9, 1).unwrap(); let sample = [ ArrayIndex::new_ij(0, 0), ArrayIndex::new_ij(1, 4), @@ -97,15 +97,15 @@ mod tests { #[test] fn grid_indexer_rejects_zero_dimensions_and_overflow() { - assert!(GridIndexer::new(0, 4).is_none()); - assert!(GridIndexer::new(4, 0).is_none()); - assert!(GridIndexer::new(0, 0).is_none()); - assert!(GridIndexer::new(u64::MAX, 2).is_none()); + assert!(GridIndexer::new(0, 4, 1).is_none()); + assert!(GridIndexer::new(4, 0, 1).is_none()); + assert!(GridIndexer::new(0, 0, 1).is_none()); + assert!(GridIndexer::new(u64::MAX, 2, 1).is_none()); } #[test] fn grid_indexer_slot_of_rejects_out_of_bounds_indices() { - let grid = GridIndexer::new(3, 4).unwrap(); + let grid = GridIndexer::new(3, 4, 1).unwrap(); assert_eq!(grid.slot_of(&ArrayIndex::new_ij(0, 0)), Some(0)); assert_eq!(grid.slot_of(&ArrayIndex::new_ij(2, 3)), Some(11)); @@ -116,9 +116,9 @@ mod tests { #[test] fn grid_indexer_finalized_bits_size_matches_cell_count() { - let single = GridIndexer::new(1, 1).unwrap(); - let full_byte = GridIndexer::new(2, 4).unwrap(); - let partial_byte = GridIndexer::new(3, 3).unwrap(); + let single = GridIndexer::new(1, 1, 1).unwrap(); + let full_byte = GridIndexer::new(2, 4, 1).unwrap(); + let partial_byte = GridIndexer::new(3, 3, 1).unwrap(); assert_eq!(single.new_finalized_bits().unwrap().bits.len(), 1); assert_eq!(full_byte.new_finalized_bits().unwrap().bits.len(), 1); From b6079ec99883b60e7c8763618f868fca686f6bdb Mon Sep 17 00:00:00 2001 From: Paul Date: Sat, 16 May 2026 14:40:56 -0600 Subject: [PATCH 05/40] UPdate layout to track options dimension --- crates/revrt/src/dataset/swap.rs | 67 ++++++++++++++++++++++++++------ 1 file changed, 55 insertions(+), 12 deletions(-) diff --git a/crates/revrt/src/dataset/swap.rs b/crates/revrt/src/dataset/swap.rs index e7030c4c..b4a279aa 100644 --- a/crates/revrt/src/dataset/swap.rs +++ b/crates/revrt/src/dataset/swap.rs @@ -8,6 +8,7 @@ use std::path::Path; use tracing::{debug, trace}; use zarrs::array::ChunkGrid; +use zarrs::array::chunk_grid::regular::RegularChunkGrid; use zarrs::storage::{ ListableStorageTraits, ReadableListableStorage, ReadableWritableListableStorage, }; @@ -21,10 +22,14 @@ use crate::error::{Error, Result}; pub(super) struct SourceLayout { /// Chunk grid definition copied from the representative source array. pub(super) chunk_grid: ChunkGrid, + /// Number of chunks along the band axis. + pub(super) chunk_grid_bands: usize, /// Number of chunk rows in the source grid. pub(super) chunk_grid_rows: usize, /// Number of chunk columns in the source grid. pub(super) chunk_grid_cols: usize, + /// Number of routing options encoded on the leading band axis. + pub(super) grid_noptions: u32, /// Number of rows in the full source grid. pub(super) grid_nrows: u64, /// Number of columns in the full source grid. @@ -40,11 +45,17 @@ pub(super) struct SourceLayout { /// /// # Arguments /// `source`: Source dataset storage containing the input feature arrays. +/// `routing_option_count`: Number of routing options that will be encoded +/// on the leading band dimension of the swap arrays. /// /// # Returns -/// A `SourceLayout` describing the representative array's grid and chunking, -/// which is then reused when creating swap arrays. -pub(super) fn inspect_source_layout(source: &ReadableListableStorage) -> Result { +/// A `SourceLayout` describing the representative array's spatial shape and +/// chunking, combined with the caller-provided routing-option count used for +/// swap arrays. +pub(super) fn inspect_source_layout( + source: &ReadableListableStorage, + routing_option_count: u32, +) -> Result { let entries = source.list().map_err(|err| { Error::IO(std::io::Error::other(format!( "failed to list variables in source dataset: {err}" @@ -89,11 +100,41 @@ pub(super) fn inspect_source_layout(source: &ReadableListableStorage) -> Result< }); } - let chunk_grid_shape = representative.chunk_grid_shape(); + let chunk_shape = representative + .chunk_grid() + .chunk_shape_u64(&[0, 0, 0]) + .map_err(|error| { + Error::IO(std::io::Error::other(format!( + "failed to inspect source chunk shape: {error}" + ))) + })? + .ok_or_else(|| { + Error::IO(std::io::Error::other( + "source chunk shape was unavailable for representative array", + )) + })?; + let chunk_grid = ChunkGrid::new( + RegularChunkGrid::new( + vec![u64::from(routing_option_count), shape[1], shape[2]], + chunk_shape.try_into().map_err(|error| { + Error::IO(std::io::Error::other(format!( + "failed to convert source chunk shape: {error}" + ))) + })?, + ) + .map_err(|error| { + Error::IO(std::io::Error::other(format!( + "failed to create swap chunk grid: {error}" + ))) + })?, + ); + let chunk_grid_shape = chunk_grid.grid_shape().to_vec(); let layout = SourceLayout { - chunk_grid: representative.chunk_grid().clone(), + chunk_grid, + chunk_grid_bands: chunk_grid_shape[0] as usize, chunk_grid_rows: chunk_grid_shape[1] as usize, chunk_grid_cols: chunk_grid_shape[2] as usize, + grid_noptions: routing_option_count, grid_nrows: shape[1], grid_ncols: shape[2], }; @@ -271,12 +312,14 @@ mod tests { #[test] fn inspect_source_layout_returns_expected_grid_metadata() { - let tmp = samples::multi_variable_random(1, 8, 8, 1, 4, 4, &["A", "B", "cost"]); + let tmp = samples::multi_variable_random(2, 8, 8, 1, 4, 4, &["A", "B", "cost"]); let source: ReadableListableStorage = Arc::new(FilesystemStore::new(tmp.path()).expect("could not open test store")); - let layout = inspect_source_layout(&source).expect("source layout inspection failed"); + let layout = inspect_source_layout(&source, 2).expect("source layout inspection failed"); + assert_eq!(layout.grid_noptions, 2); + assert_eq!(layout.chunk_grid_bands, 2); assert_eq!(layout.grid_nrows, 8); assert_eq!(layout.grid_ncols, 8); assert_eq!(layout.chunk_grid_rows, 2); @@ -296,7 +339,7 @@ mod tests { let source: ReadableListableStorage = Arc::new(FilesystemStore::new(tmp.path()).expect("could not open test store")); - let error = inspect_source_layout(&source) + let error = inspect_source_layout(&source, 1) .err() .expect("expected coordinate-only dataset to be rejected"); @@ -309,7 +352,7 @@ mod tests { let source: ReadableListableStorage = Arc::new(FilesystemStore::new(tmp.path()).expect("could not open test store")); - let error = inspect_source_layout(&source) + let error = inspect_source_layout(&source, 1) .err() .expect("expected 2D representative variable to be rejected"); @@ -328,7 +371,7 @@ mod tests { let tmp = samples::multi_variable_random(1, 8, 8, 1, 4, 4, &["A", "cost"]); let source: ReadableListableStorage = Arc::new(FilesystemStore::new(tmp.path()).expect("could not open test store")); - let layout = inspect_source_layout(&source).expect("source layout inspection failed"); + let layout = inspect_source_layout(&source, 3).expect("source layout inspection failed"); let swap_dir = TempDir::new().expect("could not create temporary swap directory"); let initialized_swap = @@ -346,10 +389,10 @@ mod tests { for (layer_name, expected_dtype) in expected_layers { let array = zarrs::array::Array::open(initialized_swap.clone(), layer_name) .unwrap_or_else(|_| panic!("expected layer {layer_name} to exist")); - assert_eq!(array.shape(), &[1, 8, 8], "wrong shape for {layer_name}"); + assert_eq!(array.shape(), &[3, 8, 8], "wrong shape for {layer_name}"); assert_eq!( array.chunk_grid_shape(), - &[1, 2, 2], + &[3, 2, 2], "wrong chunk grid shape for {layer_name}" ); assert_eq!(*array.data_type(), expected_dtype); From 8d871635426a6152d89e58d761b2248898a0ab53 Mon Sep 17 00:00:00 2001 From: Paul Date: Sat, 16 May 2026 14:42:35 -0600 Subject: [PATCH 06/40] Derived writer now pulls from 3 dimensions --- crates/revrt/src/dataset/derived.rs | 113 +++++++++++++++++----------- 1 file changed, 68 insertions(+), 45 deletions(-) diff --git a/crates/revrt/src/dataset/derived.rs b/crates/revrt/src/dataset/derived.rs index 3ab9f316..316f0548 100644 --- a/crates/revrt/src/dataset/derived.rs +++ b/crates/revrt/src/dataset/derived.rs @@ -7,7 +7,7 @@ use std::sync::RwLock; -use ndarray::Array2; +use ndarray::Array3; use tracing::trace; use zarrs::storage::{ReadableListableStorage, ReadableWritableListableStorage}; @@ -29,8 +29,8 @@ pub(super) struct DerivedDataWriter { source: ReadableListableStorage, /// Writable swap storage where derived arrays are materialized. swap: ReadableWritableListableStorage, - /// Boolean materialization state indexed by chunk row and chunk column. - swap_chunk_idx: RwLock>, + /// Boolean materialization state indexed by band, row, and column chunk. + swap_chunk_idx: RwLock>, /// Barrier layers that always behave as hard exclusions. hard_barrier_layers: Vec, /// Soft barrier layers grouped by importance in ascending retry order. @@ -63,8 +63,15 @@ impl DerivedDataWriter { let hard_barrier_layers = cost_function.hard_barrier_layers(); let soft_barrier_groups = cost_function.soft_barrier_groups(); let cost_function = cost_function.without_barriers(); - let swap_chunk_idx = - Array2::from_elem((layout.chunk_grid_rows, layout.chunk_grid_cols), false).into(); + let swap_chunk_idx = Array3::from_elem( + ( + layout.chunk_grid_bands, + layout.chunk_grid_rows, + layout.chunk_grid_cols, + ), + false, + ) + .into(); Self { source, @@ -79,23 +86,27 @@ impl DerivedDataWriter { /// Materialize every derived array for a single chunk. /// /// This computes both cost layers and all barrier masks for the chunk - /// identified by the chunk-grid coordinates `ci` and `cj`, then stores - /// the results into the swap dataset. + /// identified by the chunk-grid coordinates `cb`, `ci`, and `cj`, then + /// stores the results into the swap dataset. /// /// # Arguments + /// `cb`: Chunk band index in the swap dataset. /// `ci`: Chunk row index in the swap dataset. /// `cj`: Chunk column index in the swap dataset. - fn materialize_chunk(&self, ci: u64, cj: u64) { - trace!("Creating a LazySubset for ({}, {})", ci, cj); + fn materialize_chunk(&self, cb: u64, ci: u64, cj: u64) { + trace!("Creating a LazySubset for ({}, {}, {})", cb, ci, cj); let variable = zarrs::array::Array::open(self.swap.clone(), "/cost").unwrap(); - let subset = variable.chunk_subset(&[0, ci, cj]).unwrap(); - let chunk_subset = - zarrs::array_subset::ArraySubset::new_with_ranges(&[0..1, ci..(ci + 1), cj..(cj + 1)]); + let subset = variable.chunk_subset(&[cb, ci, cj]).unwrap(); + let chunk_subset = zarrs::array_subset::ArraySubset::new_with_ranges(&[ + cb..(cb + 1), + ci..(ci + 1), + cj..(cj + 1), + ]); let mut data = LazySubset::::new(self.source.clone(), subset.clone()); - self.calculate_chunk_cost_single_layer(ci, cj, &mut data, &chunk_subset, true); - self.calculate_chunk_cost_single_layer(ci, cj, &mut data, &chunk_subset, false); + self.calculate_chunk_cost_single_layer(cb, ci, cj, &mut data, &chunk_subset, true); + self.calculate_chunk_cost_single_layer(cb, ci, cj, &mut data, &chunk_subset, false); self.calculate_chunk_hard_barrier_mask(&mut data, &subset, &chunk_subset); self.calculate_chunk_cumulative_soft_barrier_masks(&mut data, &subset, &chunk_subset); } @@ -107,6 +118,7 @@ impl DerivedDataWriter { /// destination array in the swap dataset. /// /// # Arguments + /// `cb`: Chunk band index in the swap dataset. /// `ci`: Chunk row index in the swap dataset. /// `cj`: Chunk column index in the swap dataset. /// `features`: Lazily loaded source features for the target chunk. @@ -116,6 +128,7 @@ impl DerivedDataWriter { /// otherwise compute length-dependent terms. fn calculate_chunk_cost_single_layer( &self, + cb: u64, ci: u64, cj: u64, features: &mut LazySubset, @@ -125,13 +138,16 @@ impl DerivedDataWriter { let output; let layer_name; if is_invariant { - trace!("Calculating invariant cost for chunk ({}, {})", ci, cj); + trace!( + "Calculating invariant cost for chunk ({}, {}, {})", + cb, ci, cj + ); output = self.cost_function.compute(features, true); layer_name = "/cost_invariant"; } else { trace!( - "Calculating length-dependent cost for chunk ({}, {})", - ci, cj + "Calculating length-dependent cost for chunk ({}, {}, {})", + cb, ci, cj ); output = self.cost_function.compute(features, false); layer_name = "/cost"; @@ -141,7 +157,7 @@ impl DerivedDataWriter { let cost = zarrs::array::Array::open(self.swap.clone(), layer_name).unwrap(); cost.store_metadata().unwrap(); - let chunk_indices: Vec = vec![0, ci, cj]; + let chunk_indices: Vec = vec![cb, ci, cj]; trace!("Storing chunk at {:?}", chunk_indices); trace!("Target chunk subset: {:?}", chunk_subset); cost.store_chunks_ndarray(chunk_subset, output).unwrap(); @@ -270,35 +286,42 @@ impl DerivedDataMaterializer for DerivedDataWriter { chunks.num_elements_usize() ); - for ci in chunks.start()[1]..(chunks.start()[1] + chunks.shape()[1]) { - for cj in chunks.start()[2]..(chunks.start()[2] + chunks.shape()[2]) { - trace!( - "Checking if derived data for chunk ({}, {}) has been calculated", - ci, cj - ); - if self.swap_chunk_idx.read().unwrap()[[ci as usize, cj as usize]] { - trace!("Derived data for chunk ({}, {}) already calculated", ci, cj); - continue; - } - - let mut chunk_idx = self - .swap_chunk_idx - .write() - .expect("Failed to acquire write lock"); - if chunk_idx[[ci as usize, cj as usize]] { + for cb in chunks.start()[0]..(chunks.start()[0] + chunks.shape()[0]) { + for ci in chunks.start()[1]..(chunks.start()[1] + chunks.shape()[1]) { + for cj in chunks.start()[2]..(chunks.start()[2] + chunks.shape()[2]) { trace!( - "Derived data for chunk ({}, {}) already calculated while waiting for the lock", - ci, cj - ); - } else { - self.materialize_chunk(ci, cj); - chunk_idx[[ci as usize, cj as usize]] = true; - trace!( - "Recorded derived data for chunk ({}, {}) as calculated. Total number of computed chunks: {}", - ci, - cj, - chunk_idx.iter().filter(|&&value| value).count() + "Checking if derived data for chunk ({}, {}, {}) has been calculated", + cb, ci, cj ); + if self.swap_chunk_idx.read().unwrap()[[cb as usize, ci as usize, cj as usize]] + { + trace!( + "Derived data for chunk ({}, {}, {}) already calculated", + cb, ci, cj + ); + continue; + } + + let mut chunk_idx = self + .swap_chunk_idx + .write() + .expect("Failed to acquire write lock"); + if chunk_idx[[cb as usize, ci as usize, cj as usize]] { + trace!( + "Derived data for chunk ({}, {}, {}) already calculated while waiting for the lock", + cb, ci, cj + ); + } else { + self.materialize_chunk(cb, ci, cj); + chunk_idx[[cb as usize, ci as usize, cj as usize]] = true; + trace!( + "Recorded derived data for chunk ({}, {}, {}) as calculated. Total number of computed chunks: {}", + cb, + ci, + cj, + chunk_idx.iter().filter(|&&value| value).count() + ); + } } } } From e8a8384db8c45153fe20cf56c8841cce6ede11cc Mon Sep 17 00:00:00 2001 From: Paul Date: Sat, 16 May 2026 14:43:27 -0600 Subject: [PATCH 07/40] Pass correct function args --- crates/revrt/src/dataset/mod.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/revrt/src/dataset/mod.rs b/crates/revrt/src/dataset/mod.rs index 7b8c4159..ad853ac4 100644 --- a/crates/revrt/src/dataset/mod.rs +++ b/crates/revrt/src/dataset/mod.rs @@ -126,12 +126,16 @@ impl Dataset { ) -> Result { debug!("Opening dataset: {:?}", path.as_ref()); let soft_barrier_group_count = cost_function.soft_barrier_groups().len(); + let routing_option_count = + u32::try_from(cost_function.routing_options.len()).map_err(|_| { + crate::error::Error::IO(std::io::Error::other("routing option count exceeds u32")) + })?; let filesystem = zarrs::filesystem::FilesystemStore::new(path).expect("could not open filesystem store"); let source: ReadableListableStorage = std::sync::Arc::new(filesystem); - let source_layout = inspect_source_layout(&source)?; + let source_layout = inspect_source_layout(&source, routing_option_count)?; let swap = initialize_swap(swap_fp, &source_layout, soft_barrier_group_count)?; let derived_data_writer = From 4e961a69dec854fcad4416b3ff20f8849f47eb54 Mon Sep 17 00:00:00 2001 From: Paul Date: Sat, 16 May 2026 14:45:07 -0600 Subject: [PATCH 08/40] Reader is now option-aware --- crates/revrt/src/dataset/reader.rs | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/crates/revrt/src/dataset/reader.rs b/crates/revrt/src/dataset/reader.rs index d7aba75d..7863074a 100644 --- a/crates/revrt/src/dataset/reader.rs +++ b/crates/revrt/src/dataset/reader.rs @@ -59,6 +59,8 @@ pub(super) struct NeighborhoodReader { hard_barrier_cache: ChunkCacheDecodedLruSizeLimit, /// Decoded chunk caches for cumulative soft barrier masks by retry state. cumulative_soft_barrier_caches: Vec, + /// Number of routing options on the leading band axis. + grid_noptions: u32, /// Number of rows in the routing grid. grid_nrows: u64, /// Number of columns in the routing grid. @@ -135,6 +137,7 @@ impl NeighborhoodReader { cost_invariant_cache, hard_barrier_cache, cumulative_soft_barrier_caches, + grid_noptions: layout.grid_noptions, grid_nrows: layout.grid_nrows, grid_ncols: layout.grid_ncols, }) @@ -161,9 +164,12 @@ impl NeighborhoodReader { index: &ArrayIndex, data_materializer: &impl DerivedDataMaterializer, ) -> Vec<(ArrayIndex, f32)> { - let &ArrayIndex { i, j } = index; + let &ArrayIndex { i, j, option } = index; - trace!("Getting 3x3 neighborhood for (i={}, j={})", i, j); + trace!( + "Getting 3x3 neighborhood for (i={}, j={}, option={})", + i, j, option + ); trace!("Opening cost dataset via cache"); let cost_array = self.cost_cache.array(); @@ -220,7 +226,14 @@ impl NeighborhoodReader { } else { v }; - (ArrayIndex { i: *ir, j: *jr }, scaled + inv_cost) + ( + ArrayIndex { + i: *ir, + j: *jr, + option, + }, + scaled + inv_cost, + ) }) .collect::>(); @@ -323,7 +336,7 @@ impl NeighborhoodReader { }; let subset = zarrs::array_subset::ArraySubset::new_with_ranges(&[ - 0..1, + u64::from(option)..u64::from(option) + 1, i_range.clone(), j_range.clone(), ]); From 2fe04c8f9759b16c7ecd6c88421b239db797f57a Mon Sep 17 00:00:00 2001 From: Paul Date: Sat, 16 May 2026 15:10:29 -0600 Subject: [PATCH 09/40] Rename struct --- crates/revrt/src/dataset/mod.rs | 6 +++--- crates/revrt/src/dataset/reader.rs | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/crates/revrt/src/dataset/mod.rs b/crates/revrt/src/dataset/mod.rs index ad853ac4..354b010e 100644 --- a/crates/revrt/src/dataset/mod.rs +++ b/crates/revrt/src/dataset/mod.rs @@ -31,7 +31,7 @@ use crate::cost::{BarrierLayer, CostFunction}; use crate::error::Result; use derived::DerivedDataWriter; pub(crate) use lazy_subset::LazySubset; -use reader::NeighborhoodReader; +use reader::DerivedDataReader; use swap::{initialize_swap, inspect_source_layout}; /// Manage source features together with derived swap-backed routing data. @@ -53,7 +53,7 @@ pub(super) struct Dataset { /// Derived-data materializer responsible for chunk tracking and writes. derived_data_writer: DerivedDataWriter, /// Reader responsible for cached neighborhood access to derived data. - neighborhood_reader: NeighborhoodReader, + neighborhood_reader: DerivedDataReader, /// Shape of the source routing grid as `(rows, cols, options)`. pub(super) grid_shape: (u64, u64, u32), } @@ -141,7 +141,7 @@ impl Dataset { let derived_data_writer = DerivedDataWriter::new(&source_layout, source.clone(), swap.clone(), cost_function); - let neighborhood_reader = NeighborhoodReader::open( + let neighborhood_reader = DerivedDataReader::open( swap.clone(), cache_size, soft_barrier_group_count, diff --git a/crates/revrt/src/dataset/reader.rs b/crates/revrt/src/dataset/reader.rs index 7863074a..517fa2c1 100644 --- a/crates/revrt/src/dataset/reader.rs +++ b/crates/revrt/src/dataset/reader.rs @@ -45,12 +45,12 @@ struct CacheBudgets { per_soft_barrier_cache: u64, } -/// Cached access to derived 3x3 neighborhoods from the swap dataset. +/// Cached access to derived data from the swap dataset. /// /// The reader keeps decoded chunk caches for each derived array needed during /// routing so repeated neighborhood lookups can avoid reopening and decoding /// the same swap chunks. -pub(super) struct NeighborhoodReader { +pub(super) struct DerivedDataReader { /// Decoded chunk cache for the main per-cell routing cost. cost_cache: ChunkCacheDecodedLruSizeLimit, /// Decoded chunk cache for invariant movement costs. @@ -67,7 +67,7 @@ pub(super) struct NeighborhoodReader { grid_ncols: u64, } -impl NeighborhoodReader { +impl DerivedDataReader { /// Open cached readers for the derived swap arrays. /// /// This initializes one decoded chunk cache per derived array used during @@ -84,7 +84,7 @@ impl NeighborhoodReader { /// `layout`: Source grid layout metadata used to record dataset shape. /// /// # Returns - /// A `NeighborhoodReader` with initialized chunk caches for every derived + /// A `DerivedDataReader` with initialized chunk caches for every derived /// neighborhood array. pub(super) fn open( swap: ReadableWritableListableStorage, @@ -738,7 +738,7 @@ mod tests { soft_retry_one_values, ); - let reader = NeighborhoodReader::open(swap, 90, 1, layout).expect("reader should open"); + let reader = DerivedDataReader::open(swap, 90, 1, layout).expect("reader should open"); ReaderFixture { _source_tmp: source_tmp, @@ -796,6 +796,6 @@ mod tests { struct ReaderFixture { _source_tmp: TempDir, _swap_tmp: TempDir, - reader: NeighborhoodReader, + reader: DerivedDataReader, } } From 044bc747dfa92a321c4eaf5449679bb65cd5e4c5 Mon Sep 17 00:00:00 2001 From: Paul Date: Sat, 16 May 2026 15:11:27 -0600 Subject: [PATCH 10/40] Rename var --- crates/revrt/src/dataset/mod.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/revrt/src/dataset/mod.rs b/crates/revrt/src/dataset/mod.rs index 354b010e..f7547f97 100644 --- a/crates/revrt/src/dataset/mod.rs +++ b/crates/revrt/src/dataset/mod.rs @@ -52,8 +52,8 @@ pub(super) struct Dataset { cost_path: Option, /// Derived-data materializer responsible for chunk tracking and writes. derived_data_writer: DerivedDataWriter, - /// Reader responsible for cached neighborhood access to derived data. - neighborhood_reader: DerivedDataReader, + /// Reader responsible for cached access to derived data. + derived_data_reader: DerivedDataReader, /// Shape of the source routing grid as `(rows, cols, options)`. pub(super) grid_shape: (u64, u64, u32), } @@ -141,20 +141,20 @@ impl Dataset { let derived_data_writer = DerivedDataWriter::new(&source_layout, source.clone(), swap.clone(), cost_function); - let neighborhood_reader = DerivedDataReader::open( + let derived_data_reader = DerivedDataReader::open( swap.clone(), cache_size, soft_barrier_group_count, source_layout, )?; - let grid_shape = neighborhood_reader.grid_shape(); + let grid_shape = derived_data_reader.grid_shape(); trace!("Dataset opened successfully"); Ok(Self { source, cost_path: None, derived_data_writer, - neighborhood_reader, + derived_data_reader, grid_shape, }) } @@ -171,7 +171,7 @@ impl Dataset { /// A vector of neighboring indices paired with movement costs from the /// center cell. pub(super) fn get_3x3(&self, index: &ArrayIndex) -> Vec<(ArrayIndex, f32)> { - self.neighborhood_reader + self.derived_data_reader .get_3x3(index, &self.derived_data_writer) } @@ -195,7 +195,7 @@ impl Dataset { ) -> Vec { let retry_state = dropped_soft_groups.min(self.derived_data_writer.soft_barrier_groups.len()); - self.neighborhood_reader.get_3x3_soft_barrier_cells( + self.derived_data_reader.get_3x3_soft_barrier_cells( index, retry_state, &self.derived_data_writer, From 6b006fcc5c588ac2303a8213b4d0bcba0f57df9f Mon Sep 17 00:00:00 2001 From: Paul Date: Sat, 16 May 2026 15:15:50 -0600 Subject: [PATCH 11/40] Add `get_cell_cost` for derived data reader --- crates/revrt/src/dataset/reader.rs | 47 +++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/crates/revrt/src/dataset/reader.rs b/crates/revrt/src/dataset/reader.rs index 517fa2c1..0704043f 100644 --- a/crates/revrt/src/dataset/reader.rs +++ b/crates/revrt/src/dataset/reader.rs @@ -287,7 +287,52 @@ impl DerivedDataReader { barrier_cells } - /// Return the grid shape backing this reader as `(rows, cols)`. + /// Return the center-cell entry cost for a single option state. + /// + /// The returned value includes the length-dependent center-cell cost and + /// the invariant adders for the destination option. Hard barriers and + /// invalid costs return `None`. + pub(super) fn get_cell_cost( + &self, + index: &ArrayIndex, + data_materializer: &impl DerivedDataMaterializer, + ) -> Option { + let subset = zarrs::array_subset::ArraySubset::new_with_ranges(&[ + u64::from(index.option)..u64::from(index.option) + 1, + index.i..index.i + 1, + index.j..index.j + 1, + ]); + let cost_array = self.cost_cache.array(); + data_materializer.ensure_derived_data_for_subset(&cost_array, &subset); + + let cost = self + .cost_cache + .retrieve_array_subset_elements::(&subset, &CodecOptions::default()) + .ok()? + .into_iter() + .next()?; + let is_hard_barrier = data_materializer.has_hard_barriers() + && self + .hard_barrier_cache + .retrieve_array_subset_elements::(&subset, &CodecOptions::default()) + .ok()? + .into_iter() + .next()?; + + if is_hard_barrier || cost.is_nan() || cost <= 0.0 { + None + } else { + let invariant = self + .cost_invariant_cache + .retrieve_array_subset_elements::(&subset, &CodecOptions::default()) + .ok()? + .into_iter() + .next()?; + Some(cost + invariant) + } + } + + /// Return the grid shape backing this reader as `(rows, cols, options)`. /// /// # Returns /// The routing grid dimensions recorded when the reader was opened. From b1d164d0be188027dc8d3e44018ef6be5eb507e8 Mon Sep 17 00:00:00 2001 From: Paul Date: Sat, 16 May 2026 15:18:04 -0600 Subject: [PATCH 12/40] Add comment --- crates/revrt/src/dataset/reader.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/revrt/src/dataset/reader.rs b/crates/revrt/src/dataset/reader.rs index 0704043f..628cbeb0 100644 --- a/crates/revrt/src/dataset/reader.rs +++ b/crates/revrt/src/dataset/reader.rs @@ -204,7 +204,9 @@ impl DerivedDataReader { } }) .unwrap(); + if center.2 { + // center cell is barrier, so no neighbors return Vec::new(); } trace!("Center point: {:?}", center); From c6ccdf7afee7af1eb918466eb390af3ece1fef95 Mon Sep 17 00:00:00 2001 From: Paul Date: Sat, 16 May 2026 15:19:07 -0600 Subject: [PATCH 13/40] Dataset can now get center cell cost --- crates/revrt/src/dataset/mod.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/revrt/src/dataset/mod.rs b/crates/revrt/src/dataset/mod.rs index f7547f97..e5e8a605 100644 --- a/crates/revrt/src/dataset/mod.rs +++ b/crates/revrt/src/dataset/mod.rs @@ -202,6 +202,12 @@ impl Dataset { ) } + /// Return the center-cell entry cost for a single option state. + pub(super) fn get_cell_cost(&self, index: &ArrayIndex) -> Option { + self.derived_data_reader + .get_cell_cost(index, &self.derived_data_writer) + } + /// Return the number of soft barrier importance groups. /// /// # Returns From ca9b87d63b75136a4eb00c8c1cb00eb15b714887 Mon Sep 17 00:00:00 2001 From: Paul Date: Sat, 16 May 2026 15:57:43 -0600 Subject: [PATCH 14/40] Allow dataset to get non-cost layer cells --- crates/revrt/src/dataset/mod.rs | 62 +++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/crates/revrt/src/dataset/mod.rs b/crates/revrt/src/dataset/mod.rs index e5e8a605..21fb613c 100644 --- a/crates/revrt/src/dataset/mod.rs +++ b/crates/revrt/src/dataset/mod.rs @@ -23,7 +23,9 @@ mod swap; use std::path::PathBuf; +use num_traits::AsPrimitive; use tracing::{debug, info, trace}; +use zarrs::array::{Array, DataType, ElementOwned}; use zarrs::storage::ReadableListableStorage; use crate::ArrayIndex; @@ -208,6 +210,28 @@ impl Dataset { .get_cell_cost(index, &self.derived_data_writer) } + /// Return a single source-layer cell as `f32` for the requested index. + pub(super) fn get_source_cell_value( + &self, + layer_name: &str, + index: &ArrayIndex, + ) -> Option { + let array = Array::open(self.source.clone(), &format!("/{layer_name}")).ok()?; + let subset = match array.shape().len() { + 2 => zarrs::array_subset::ArraySubset::new_with_ranges(&[ + index.i..(index.i + 1), + index.j..(index.j + 1), + ]), + 3 => zarrs::array_subset::ArraySubset::new_with_ranges(&[ + u64::from(index.option)..(u64::from(index.option) + 1), + index.i..(index.i + 1), + index.j..(index.j + 1), + ]), + _ => return None, + }; + read_source_cell_as_f32(&array, &subset) + } + /// Return the number of soft barrier importance groups. /// /// # Returns @@ -218,6 +242,44 @@ impl Dataset { } } +fn read_source_cell_as_f32( + array: &Array, + subset: &zarrs::array_subset::ArraySubset, +) -> Option +where + TStorage: ?Sized + zarrs::storage::ReadableListableStorageTraits + 'static, +{ + match array.data_type() { + DataType::Float32 => read_typed_cell::(array, subset), + DataType::Float64 => read_typed_cell::(array, subset), + DataType::Int8 => read_typed_cell::(array, subset), + DataType::Int16 => read_typed_cell::(array, subset), + DataType::Int32 => read_typed_cell::(array, subset), + DataType::Int64 => read_typed_cell::(array, subset), + DataType::UInt8 => read_typed_cell::(array, subset), + DataType::UInt16 => read_typed_cell::(array, subset), + DataType::UInt32 => read_typed_cell::(array, subset), + DataType::UInt64 => read_typed_cell::(array, subset), + _ => None, + } +} + +fn read_typed_cell( + array: &Array, + subset: &zarrs::array_subset::ArraySubset, +) -> Option +where + T: ElementOwned + Clone + AsPrimitive, + TStorage: ?Sized + zarrs::storage::ReadableListableStorageTraits + 'static, +{ + array + .retrieve_array_subset_elements::(subset) + .ok()? + .into_iter() + .next() + .map(|value| value.as_()) +} + #[cfg(test)] /// Construct a `LazySubset` helper for tests. /// From 2ad080cde66cef81ef55e506ed7062a27de16594 Mon Sep 17 00:00:00 2001 From: Paul Date: Sat, 16 May 2026 16:02:53 -0600 Subject: [PATCH 15/40] Add comment --- crates/revrt/src/dataset/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/revrt/src/dataset/mod.rs b/crates/revrt/src/dataset/mod.rs index 21fb613c..24d68962 100644 --- a/crates/revrt/src/dataset/mod.rs +++ b/crates/revrt/src/dataset/mod.rs @@ -211,6 +211,7 @@ impl Dataset { } /// Return a single source-layer cell as `f32` for the requested index. + /// Useful for reading non-cost (i.e. not derived) features pub(super) fn get_source_cell_value( &self, layer_name: &str, From 7274b1769e936e8ef5b30e197d2fb8ec65056baf Mon Sep 17 00:00:00 2001 From: Paul Date: Sat, 16 May 2026 16:16:04 -0600 Subject: [PATCH 16/40] Grid indexer is now 3D --- .../revrt/src/network/long_range/utilities.rs | 110 +++++++++++++++++- 1 file changed, 105 insertions(+), 5 deletions(-) diff --git a/crates/revrt/src/network/long_range/utilities.rs b/crates/revrt/src/network/long_range/utilities.rs index ca28c9e0..5630e942 100644 --- a/crates/revrt/src/network/long_range/utilities.rs +++ b/crates/revrt/src/network/long_range/utilities.rs @@ -4,33 +4,41 @@ use crate::ArrayIndex; pub(super) struct GridIndexer { nrows: u64, ncols: u64, + noptions: u32, total_cells: u64, } impl GridIndexer { - pub(super) fn new(nrows: u64, ncols: u64) -> Option { - if nrows == 0 || ncols == 0 { + pub(super) fn new(nrows: u64, ncols: u64, noptions: u32) -> Option { + if nrows == 0 || ncols == 0 || noptions == 0 { return None; } - let total_cells = nrows.checked_mul(ncols)?; + let total_cells = nrows.checked_mul(ncols)?.checked_mul(u64::from(noptions))?; Some(Self { nrows, ncols, + noptions, total_cells, }) } pub(super) fn slot_of(&self, index: &ArrayIndex) -> Option { - if index.i >= self.nrows || index.j >= self.ncols { + if index.i >= self.nrows || index.j >= self.ncols || index.option >= self.noptions { return None; } - let linear = index.i.checked_mul(self.ncols)?.checked_add(index.j)?; + let planar = index.i.checked_mul(self.ncols)?.checked_add(index.j)?; + let linear = planar + .checked_mul(u64::from(self.noptions))? + .checked_add(u64::from(index.option))?; usize::try_from(linear).ok() } pub(super) fn index_of(&self, slot: usize) -> ArrayIndex { let linear = slot as u64; + let option = (linear % u64::from(self.noptions)) as u32; + let planar = linear / u64::from(self.noptions); + ArrayIndex { i: planar / self.ncols, j: planar % self.ncols, @@ -125,6 +133,98 @@ mod tests { assert_eq!(partial_byte.new_finalized_bits().unwrap().bits.len(), 2); } + #[test] + fn grid_indexer_round_trip_with_options() { + let grid = GridIndexer::new(7, 9, 3).unwrap(); + let sample = [ + ArrayIndex { + i: 0, + j: 0, + option: 0, + }, + ArrayIndex { + i: 1, + j: 4, + option: 1, + }, + ArrayIndex { + i: 3, + j: 8, + option: 2, + }, + ArrayIndex { + i: 6, + j: 7, + option: 0, + }, + ]; + + for index in sample { + let slot = grid.slot_of(&index).unwrap(); + assert_eq!(grid.index_of(slot), index); + } + } + + #[test] + fn grid_indexer_rejects_invalid_option_dimensions_and_overflow() { + assert!(GridIndexer::new(0, 4, 2).is_none()); + assert!(GridIndexer::new(4, 0, 2).is_none()); + assert!(GridIndexer::new(4, 4, 0).is_none()); + assert!(GridIndexer::new(u64::MAX, 2, 2).is_none()); + } + + #[test] + fn grid_indexer_slot_of_rejects_out_of_bounds_indices_with_options() { + let grid = GridIndexer::new(3, 4, 2).unwrap(); + + assert_eq!( + grid.slot_of(&ArrayIndex { + i: 0, + j: 0, + option: 0 + }), + Some(0) + ); + assert_eq!( + grid.slot_of(&ArrayIndex { + i: 2, + j: 3, + option: 1 + }), + Some(23) + ); + assert_eq!( + grid.slot_of(&ArrayIndex { + i: 3, + j: 0, + option: 0 + }), + None + ); + assert_eq!( + grid.slot_of(&ArrayIndex { + i: 0, + j: 4, + option: 0 + }), + None + ); + assert_eq!( + grid.slot_of(&ArrayIndex { + i: 0, + j: 0, + option: 2 + }), + None + ); + } + + #[test] + fn grid_indexer_finalized_bits_size_matches_cell_count_with_options() { + let grid = GridIndexer::new(2, 4, 3).unwrap(); + assert_eq!(grid.new_finalized_bits().unwrap().bits.len(), 3); + } + #[test] fn finalized_bits_new_handles_zero_cells() { let bits = FinalizedBits::new(0).unwrap(); From 53ad2fc99f3d654b883bf81c833ee62e1cd0895a Mon Sep 17 00:00:00 2001 From: Paul Date: Sat, 16 May 2026 16:19:56 -0600 Subject: [PATCH 17/40] Add helper methods --- crates/revrt/src/routing/scenario.rs | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/crates/revrt/src/routing/scenario.rs b/crates/revrt/src/routing/scenario.rs index e93151a0..f946c732 100644 --- a/crates/revrt/src/routing/scenario.rs +++ b/crates/revrt/src/routing/scenario.rs @@ -164,6 +164,34 @@ impl Scenario { dropped_barrier_layers } + /// Resolve the driver multiplier for a cell state. + /// + /// # Arguments + /// `position`: Grid index whose routing option and source-layer values + /// should be evaluated against the configured driver rules. + /// + /// # Returns + /// The multiplier for the state's routing option, or `None` when that + /// option is excluded at the given position. + fn driver_multiplier(&self, position: &ArrayIndex) -> Option { + self.driver_rules.multiplier(position.option, |layer_name| { + self.dataset.get_source_cell_value(layer_name, position) + }) + } + + /// Return the integer-encoded cost of changing routing options. + /// + /// # Arguments + /// `from_option`: Source routing option index. + /// `to_option`: Destination routing option index. + /// + /// # Returns + /// The configured transition cost between the two options, converted to + /// the internal integer routing-cost representation. + fn transition_cost(&self, from_option: u32, to_option: u32) -> u64 { + cost_as_u64(self.transition_costs.cost(from_option, to_option)) + } + /// Return the scenario grid dimensions. /// /// # Returns From 3e87c430a019650dd467583ea156bd770e7e774e Mon Sep 17 00:00:00 2001 From: Paul Date: Sat, 16 May 2026 16:22:39 -0600 Subject: [PATCH 18/40] Add `allowed_states_at` --- crates/revrt/src/routing/scenario.rs | 41 ++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/crates/revrt/src/routing/scenario.rs b/crates/revrt/src/routing/scenario.rs index f946c732..64a191ca 100644 --- a/crates/revrt/src/routing/scenario.rs +++ b/crates/revrt/src/routing/scenario.rs @@ -164,6 +164,47 @@ impl Scenario { dropped_barrier_layers } + /// Return the routing states allowed at a grid position. + /// + /// # Arguments + /// `position`: Grid position whose routing options should be checked. + /// `dropped_soft_groups`: Number of lowest-importance soft barrier + /// groups that should be ignored for this retry. + /// + /// # Returns + /// All routing-option states at the given cell that are not blocked by + /// active soft barriers, hard barriers, or driver exclusions. + pub(super) fn allowed_states_at( + &self, + position: &ArrayIndex, + dropped_soft_groups: usize, + ) -> Vec { + let (_, _, noptions) = self.grid_shape(); + + (0..noptions) + .filter_map(|option| { + let candidate = ArrayIndex { + i: position.i, + j: position.j, + option, + }; + let soft_barrier_cells: HashSet<_> = self + .dataset + .get_3x3_soft_barrier_cells(&candidate, dropped_soft_groups) + .into_iter() + .collect(); + if soft_barrier_cells.contains(&candidate) { + return None; + } + + self.dataset + .get_cell_cost(&candidate) // checks for hard barriers + .filter(|_| self.driver_multiplier(&candidate).is_some()) // drops `None` values from `get_cell_cost` + .map(|_| candidate) // drops `None` values from `driver_multiplier` + }) + .collect() + } + /// Resolve the driver multiplier for a cell state. /// /// # Arguments From d9f236877dd672c2258d7c64bb7de9e897bd0c60 Mon Sep 17 00:00:00 2001 From: Paul Date: Sat, 16 May 2026 16:26:47 -0600 Subject: [PATCH 19/40] Successors now determined by driver layer and can include transitions between routing options --- crates/revrt/src/routing/scenario.rs | 72 +++++++++++++++++++++++++--- 1 file changed, 66 insertions(+), 6 deletions(-) diff --git a/crates/revrt/src/routing/scenario.rs b/crates/revrt/src/routing/scenario.rs index 64a191ca..b90d9070 100644 --- a/crates/revrt/src/routing/scenario.rs +++ b/crates/revrt/src/routing/scenario.rs @@ -18,6 +18,7 @@ use std::path::PathBuf; use tracing::trace; use super::cost_as_u64; +use crate::cost::{DriverRuleSet, TransitionCostTable}; use crate::routing::features::Features; use crate::{ArrayIndex, Result}; @@ -30,6 +31,8 @@ use crate::{ArrayIndex, Result}; pub(super) struct Scenario { /// Derived dataset containing cost arrays and barrier masks. pub dataset: crate::dataset::Dataset, + transition_costs: TransitionCostTable, + driver_rules: DriverRuleSet, #[allow(dead_code)] /// Source feature metadata kept alive for scenario lifetime management. features: Features, @@ -56,6 +59,8 @@ impl Scenario { ) -> Result { trace!("Opening scenario with: {:?}", store_path.as_ref()); + let driver_rules = cost_function.driver_rule_set()?; + let transition_costs = cost_function.transition_cost_table()?; let features = Features::open(&store_path)?; let dataset = crate::dataset::Dataset::open_with_swap( store_path, @@ -64,7 +69,12 @@ impl Scenario { swap_fp, )?; - Ok(Self { dataset, features }) + Ok(Self { + dataset, + transition_costs, + driver_rules, + features, + }) } /// Open a scenario that derives routing data directly from the source. @@ -84,10 +94,17 @@ impl Scenario { ) -> Result { trace!("Opening scenario with: {:?}", store_path.as_ref()); + let driver_rules = cost_function.driver_rule_set()?; + let transition_costs = cost_function.transition_cost_table()?; let features = Features::open(&store_path)?; let dataset = crate::dataset::Dataset::open(store_path, cost_function, cache_size)?; - Ok(Self { dataset, features }) + Ok(Self { + dataset, + transition_costs, + driver_rules, + features, + }) } /// Return retry-aware successor cells for a routing attempt. @@ -110,7 +127,7 @@ impl Scenario { position: &ArrayIndex, dropped_soft_groups: usize, ) -> Vec<(ArrayIndex, u64)> { - let neighbors = self.dataset.get_3x3(position); + let spatial_neighbors = self.dataset.get_3x3(position); let soft_barrier_cells: HashSet<_> = self .dataset .get_3x3_soft_barrier_cells(position, dropped_soft_groups) @@ -121,11 +138,54 @@ impl Scenario { return Vec::new(); } - let neighbors = neighbors + if self.driver_multiplier(position).is_none() { + return Vec::new(); + } + + let mut neighbors = spatial_neighbors .into_iter() .filter(|(p, c)| c.is_finite() && *c > 0.0 && !soft_barrier_cells.contains(p)) - .map(|(p, c)| (p, cost_as_u64(c))) - .collect(); + .filter_map(|(p, c)| { + self.driver_multiplier(&p) + .map(|multiplier| (p, cost_as_u64(c * multiplier))) + }) + .collect::>(); + + let (_, _, noptions) = self.grid_shape(); + for option in 0..noptions { + if option == position.option { + continue; + } + + let destination = ArrayIndex { + i: position.i, + j: position.j, + option, + }; + let destination_soft_barriers: HashSet<_> = self + .dataset + .get_3x3_soft_barrier_cells(&destination, dropped_soft_groups) + .into_iter() + .collect(); + if destination_soft_barriers.contains(&destination) { + continue; + } + + let Some(destination_center_cost) = self.dataset.get_cell_cost(&destination) else { + continue; // destination is hard barriered + }; + let Some(driver_multiplier) = self.driver_multiplier(&destination) else { + continue; // destination is excluded by the driver + }; + + let transition_cost = self.transition_cost(position.option, option); + neighbors.push(( + destination, + transition_cost + .saturating_add(cost_as_u64(destination_center_cost * driver_multiplier)), + )); + } + trace!("Adjusting neighbors' types: {:?}", neighbors); neighbors } From b25983d67603c8abc5c57d236665ff0034ffbc6d Mon Sep 17 00:00:00 2001 From: Paul Date: Sat, 16 May 2026 16:38:04 -0600 Subject: [PATCH 20/40] Return an iter now --- crates/revrt/src/routing/scenario.rs | 46 ++++++++++++++-------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/crates/revrt/src/routing/scenario.rs b/crates/revrt/src/routing/scenario.rs index b90d9070..98df0399 100644 --- a/crates/revrt/src/routing/scenario.rs +++ b/crates/revrt/src/routing/scenario.rs @@ -238,31 +238,31 @@ impl Scenario { &self, position: &ArrayIndex, dropped_soft_groups: usize, - ) -> Vec { + ) -> impl Iterator + '_ { let (_, _, noptions) = self.grid_shape(); + let row = position.i; + let col = position.j; - (0..noptions) - .filter_map(|option| { - let candidate = ArrayIndex { - i: position.i, - j: position.j, - option, - }; - let soft_barrier_cells: HashSet<_> = self - .dataset - .get_3x3_soft_barrier_cells(&candidate, dropped_soft_groups) - .into_iter() - .collect(); - if soft_barrier_cells.contains(&candidate) { - return None; - } - - self.dataset - .get_cell_cost(&candidate) // checks for hard barriers - .filter(|_| self.driver_multiplier(&candidate).is_some()) // drops `None` values from `get_cell_cost` - .map(|_| candidate) // drops `None` values from `driver_multiplier` - }) - .collect() + (0..noptions).filter_map(move |option| { + let candidate = ArrayIndex { + i: row, + j: col, + option, + }; + let soft_barrier_cells: HashSet<_> = self + .dataset + .get_3x3_soft_barrier_cells(&candidate, dropped_soft_groups) + .into_iter() + .collect(); + if soft_barrier_cells.contains(&candidate) { + return None; + } + + self.dataset + .get_cell_cost(&candidate) // checks for hard barriers + .filter(|_| self.driver_multiplier(&candidate).is_some()) // drops `None` values from `get_cell_cost` + .map(|_| candidate) // drops `None` values from `driver_multiplier` + }) } /// Resolve the driver multiplier for a cell state. From 29312dea9101cf6dba3bea6bf68fbbd6a98f5828 Mon Sep 17 00:00:00 2001 From: Paul Date: Sat, 16 May 2026 16:38:38 -0600 Subject: [PATCH 21/40] Drop routes with barriered or driver excluded start points --- crates/revrt/src/routing/mod.rs | 69 ++++++++++++++++++++++++++++++++- 1 file changed, 68 insertions(+), 1 deletion(-) diff --git a/crates/revrt/src/routing/mod.rs b/crates/revrt/src/routing/mod.rs index 53b3169f..5c54caa8 100644 --- a/crates/revrt/src/routing/mod.rs +++ b/crates/revrt/src/routing/mod.rs @@ -214,8 +214,17 @@ fn compute_solution_for_start( ); } + let start_state = scenario + .allowed_states_at(start_point, dropped_soft_groups) + .find(|state| *state == *start_point); + + let start_point = match start_state { + Some(state) => state, + None => continue, + }; + let solution = algorithm.compute( - start_point, + &start_point, end, |p| scenario.successors_for_attempt(p, dropped_soft_groups), |p| end.contains(p), @@ -231,6 +240,64 @@ fn compute_solution_for_start( None } +// fn compute_solution_for_start( +// scenario: &Scenario, +// algorithm: &Algorithm, +// start_point: &ArrayIndex, +// end: &[ArrayIndex], +// ) -> Option> { +// let grid_shape = scenario.grid_shape(); +// let goal_requires_explicit_option = end.iter().any(|goal| goal.option != 0); + +// for dropped_soft_groups in 0..=scenario.soft_barrier_group_count() { +// if dropped_soft_groups > 0 { +// info!( +// "Retrying route from {:?} with {} soft-barrier group(s) dropped", +// start_point, dropped_soft_groups +// ); +// } + +// let start_requires_explicit_option = +// start_point.option != 0 || goal_requires_explicit_option; + +// let start_states = if start_requires_explicit_option { +// scenario +// .allowed_states_at(start_point, dropped_soft_groups) +// .into_iter() +// .filter(|state| *state == *start_point) +// .collect() +// } else { +// scenario.allowed_states_at(start_point, dropped_soft_groups) +// }; + +// let solution = start_states +// .iter() +// .filter_map(|start_state| { +// algorithm.compute( +// start_state, +// end, +// |p| scenario.successors_for_attempt(p, dropped_soft_groups), +// |p| { +// if goal_requires_explicit_option { +// end.iter().any(|goal| goal == p) +// } else { +// end.iter().any(|goal| goal.i == p.i && goal.j == p.j) +// } +// }, +// grid_shape, +// ) +// }) +// .min_by(|left, right| left.total_cost.total_cmp(&right.total_cost)); + +// if let Some(solution) = solution { +// let dropped_barrier_layers = scenario.dropped_barrier_layers(dropped_soft_groups); +// return Some(solution.record_dropped_barriers(dropped_barrier_layers)); +// } +// } + +// None +// } + const PRECISION_SCALAR: f32 = 1e4; fn cost_as_u64(cost: f32) -> u64 { let cost = cost * PRECISION_SCALAR; From 844e48cdf5bf6bd5b7135988ad96f8d18384fba8 Mon Sep 17 00:00:00 2001 From: Paul Date: Sat, 16 May 2026 16:39:34 -0600 Subject: [PATCH 22/40] Tests for new functionality --- crates/revrt/src/benchmark.rs | 16 +- crates/revrt/src/dataset/derived.rs | 202 ++++++---- crates/revrt/src/dataset/mod.rs | 166 +++++---- crates/revrt/src/dataset/reader.rs | 147 +++++++- crates/revrt/src/lib.rs | 52 ++- crates/revrt/src/routing/astar.rs | 63 ++++ crates/revrt/src/routing/mod.rs | 296 ++++++++++++++- crates/revrt/src/routing/scenario.rs | 533 +++++++++++++++++++++++---- 8 files changed, 1233 insertions(+), 242 deletions(-) diff --git a/crates/revrt/src/benchmark.rs b/crates/revrt/src/benchmark.rs index 012dfd37..44871197 100644 --- a/crates/revrt/src/benchmark.rs +++ b/crates/revrt/src/benchmark.rs @@ -18,12 +18,16 @@ pub fn bench_minimalist( ) { // temporary solution for a cost function until we have a builder let cost_json = r#"{ - "cost_layers": [ - {"layer_name": "A"}, - {"layer_name": "B", "multiplier_scalar": 100}, - {"layer_name": "A", "multiplier_layer": "B"}, - {"layer_name": "C", "multiplier_layer": "A", "multiplier_scalar": 2} - ] + "routing_options": { + "default": { + "cost_layers": [ + {"layer_name": "A"}, + {"layer_name": "B", "multiplier_scalar": 100}, + {"layer_name": "A", "multiplier_layer": "B"}, + {"layer_name": "C", "multiplier_layer": "A", "multiplier_scalar": 2} + ] + } + } }"# .to_string(); let cost_function = CostFunction::from_json(&cost_json).unwrap(); diff --git a/crates/revrt/src/dataset/derived.rs b/crates/revrt/src/dataset/derived.rs index 316f0548..65d8b75c 100644 --- a/crates/revrt/src/dataset/derived.rs +++ b/crates/revrt/src/dataset/derived.rs @@ -487,19 +487,23 @@ mod tests { let mut features = LazySubset::::new(source.clone(), subset.clone()); let cost_function = CostFunction::from_json( r#"{ - "cost_layers": [{"layer_name": "cost_length"}], - "barrier_layers": [ - { - "layer_name": "hard_barrier_a", - "barrier_operator": "eq", - "barrier_threshold": 1.0 - }, - { - "layer_name": "hard_barrier_b", - "barrier_operator": "eq", - "barrier_threshold": 1.0 + "routing_options": { + "default": { + "cost_layers": [{"layer_name": "cost_length"}], + "barrier_layers": [ + { + "layer_name": "hard_barrier_a", + "barrier_operator": "eq", + "barrier_threshold": 1.0 + }, + { + "layer_name": "hard_barrier_b", + "barrier_operator": "eq", + "barrier_threshold": 1.0 + } + ] } - ] + } }"#, ) .unwrap(); @@ -518,41 +522,45 @@ mod tests { let (_source_tmp, source) = make_source_store(); let cost_function = CostFunction::from_json( r#"{ - "cost_layers": [ - {"layer_name": "cost_length"}, - { - "layer_name": "cost_invariant_src", - "is_invariant": true + "routing_options": { + "default": { + "cost_layers": [ + {"layer_name": "cost_length"}, + { + "layer_name": "cost_invariant_src", + "is_invariant": true + } + ], + "barrier_layers": [ + { + "layer_name": "hard_barrier_a", + "barrier_operator": "eq", + "barrier_threshold": 1.0 + }, + { + "layer_name": "hard_barrier_b", + "barrier_operator": "eq", + "barrier_threshold": 1.0 + }, + { + "layer_name": "soft_barrier_low", + "barrier_operator": "eq", + "barrier_threshold": 1.0, + "barrier_importance": 1 + }, + { + "layer_name": "soft_barrier_high", + "barrier_operator": "eq", + "barrier_threshold": 1.0, + "barrier_importance": 2 + } + ] } - ], - "barrier_layers": [ - { - "layer_name": "hard_barrier_a", - "barrier_operator": "eq", - "barrier_threshold": 1.0 - }, - { - "layer_name": "hard_barrier_b", - "barrier_operator": "eq", - "barrier_threshold": 1.0 - }, - { - "layer_name": "soft_barrier_low", - "barrier_operator": "eq", - "barrier_threshold": 1.0, - "barrier_importance": 1 - }, - { - "layer_name": "soft_barrier_high", - "barrier_operator": "eq", - "barrier_threshold": 1.0, - "barrier_importance": 2 - } - ] + } }"#, ) .unwrap(); - let layout = super::super::swap::inspect_source_layout(&source).unwrap(); + let layout = super::super::swap::inspect_source_layout(&source, 1).unwrap(); let swap_tmp = TempDir::new().unwrap(); let swap = super::super::swap::initialize_swap(swap_tmp.path(), &layout, 2).unwrap(); let writer = DerivedDataWriter::new(&layout, source, swap.clone(), cost_function); @@ -561,7 +569,7 @@ mod tests { assert!(writer.has_hard_barriers()); assert_eq!(writer.soft_barrier_groups.len(), 2); - writer.materialize_chunk(0, 0); + writer.materialize_chunk(0, 0, 0); assert_eq!( read_subset_values::(&swap, "/cost", &subset), @@ -593,14 +601,18 @@ mod tests { fn materialize_chunk_extracts_hard_barriers_and_preserves_costs() { let json = r#" { - "cost_layers": [{"layer_name": "A"}], - "barrier_layers": [ - { - "layer_name": "B", - "barrier_operator": "eq", - "barrier_threshold": 1.0 + "routing_options": { + "default": { + "cost_layers": [{"layer_name": "A"}], + "barrier_layers": [ + { + "layer_name": "B", + "barrier_operator": "eq", + "barrier_threshold": 1.0 + } + ] } - ] + } } "#; @@ -617,7 +629,7 @@ mod tests { let source: ReadableListableStorage = Arc::new(FilesystemStore::new(source_dir.path()).expect("could not open source")); let cost_function = CostFunction::from_json(json).unwrap(); - let layout = inspect_source_layout(&source).expect("Error inspecting source layout"); + let layout = inspect_source_layout(&source, 1).expect("Error inspecting source layout"); let swap_dir = tempfile::TempDir::new().expect("could not create swap dir"); let swap = initialize_swap( swap_dir.path(), @@ -629,7 +641,7 @@ mod tests { assert!(writer.has_hard_barriers()); - writer.materialize_chunk(0, 0); + writer.materialize_chunk(0, 0, 0); let subset = ArraySubset::new_with_ranges(&[0..1, 0..3, 0..3]); let cost_values: Vec = Array::open(swap.clone(), "/cost") @@ -656,12 +668,14 @@ mod tests { let tmp = samples::multi_variable_random(1, 8, 8, 1, 4, 4, &["A"]); let source: ReadableListableStorage = Arc::new(FilesystemStore::new(tmp.path()).expect("could not open test store")); - let layout = inspect_source_layout(&source).expect("source layout inspection failed"); + let layout = inspect_source_layout(&source, 1).expect("source layout inspection failed"); let swap_tmp = TempDir::new().expect("could not create swap dir"); let swap = initialize_swap(swap_tmp.path(), &layout, 0) .expect("failed to initialize swap dataset"); - let cost_function = CostFunction::from_json(r#"{"cost_layers": [{"layer_name": "A"}]}"#) - .expect("failed to construct cost function"); + let cost_function = CostFunction::from_json( + r#"{"routing_options":{"default":{"cost_layers":[{"layer_name":"A"}]}}}"#, + ) + .expect("failed to construct cost function"); let writer = DerivedDataWriter::new(&layout, source, swap, cost_function); let chunk_idx = writer @@ -669,7 +683,7 @@ mod tests { .read() .expect("failed to acquire read lock"); - assert_eq!(chunk_idx.dim(), (2, 2)); + assert_eq!(chunk_idx.dim(), (1, 2, 2)); assert!(chunk_idx.iter().all(|&value| !value)); } @@ -678,7 +692,7 @@ mod tests { let tmp = samples::multi_variable_random(1, 8, 8, 1, 4, 4, &["A"]); let source: ReadableListableStorage = Arc::new(FilesystemStore::new(tmp.path()).expect("could not open test store")); - let layout = inspect_source_layout(&source).expect("source layout inspection failed"); + let layout = inspect_source_layout(&source, 1).expect("source layout inspection failed"); let readable_source: Arc = Arc::new( FilesystemStore::new(tmp.path()).expect("could not reopen readable test store"), ); @@ -687,8 +701,10 @@ mod tests { let swap_tmp = TempDir::new().expect("could not create swap dir"); let swap = initialize_swap(swap_tmp.path(), &layout, 0) .expect("failed to initialize swap dataset"); - let cost_function = CostFunction::from_json(r#"{"cost_layers": [{"layer_name": "A"}]}"#) - .expect("failed to construct cost function"); + let cost_function = CostFunction::from_json( + r#"{"routing_options":{"default":{"cost_layers":[{"layer_name":"A"}]}}}"#, + ) + .expect("failed to construct cost function"); let writer = DerivedDataWriter::new(&layout, source, swap, cost_function); let materialized = Mutex::new(Vec::new()); @@ -700,7 +716,7 @@ mod tests { .read() .expect("failed to acquire read lock"); for (ci, cj) in [(0, 0), (1, 0)] { - if chunk_idx[[ci, cj]] { + if chunk_idx[[0, ci, cj]] { materialized .lock() .expect("failed to record materialized chunk") @@ -717,7 +733,7 @@ mod tests { .read() .expect("failed to acquire read lock"); for (ci, cj) in [(0, 1), (1, 1)] { - if chunk_idx[[ci, cj]] { + if chunk_idx[[0, ci, cj]] { materialized .lock() .expect("failed to record materialized chunk") @@ -739,6 +755,66 @@ mod tests { .swap_chunk_idx .read() .expect("failed to acquire read lock"); - assert_eq!(*chunk_idx, Array2::from_elem((2, 2), true)); + assert_eq!(*chunk_idx, Array3::from_elem((1, 2, 2), true)); + } + + #[test] + fn ensure_derived_data_for_subset_materializes_each_band_chunk_once() { + let source_tmp = ZarrTestBuilder::new() + .dimensions(2, 2, 2) + .chunks(1, 2, 2) + .layer(LayerConfig::custom("A", |band, _, _| (band + 1) as f32)) + .build() + .expect("failed to create multi-band source dataset"); + let source: ReadableListableStorage = + Arc::new(FilesystemStore::new(source_tmp.path()).expect("could not open test store")); + let readable_source: Arc = Arc::new( + FilesystemStore::new(source_tmp.path()).expect("could not reopen readable test store"), + ); + let cost_function = CostFunction::from_json( + r#"{"routing_options":{"overhead":{"cost_layers":[{"layer_name":"A"}]},"underground":{"cost_layers":[{"layer_name":"A"}]}}}"#, + ) + .expect("failed to construct cost function"); + let layout = inspect_source_layout(&source, cost_function.routing_options.len() as u32) + .expect("source layout inspection failed"); + let array = + zarrs::array::Array::open(readable_source, "/A").expect("failed to open source array"); + let swap_tmp = TempDir::new().expect("could not create swap dir"); + let swap = initialize_swap(swap_tmp.path(), &layout, 0) + .expect("failed to initialize swap dataset"); + let writer = DerivedDataWriter::new(&layout, source, swap.clone(), cost_function); + + let first_option_subset = ArraySubset::new_with_ranges(&[0..1, 0..2, 0..2]); + writer.ensure_derived_data_for_subset(&array, &first_option_subset); + + { + let chunk_idx = writer + .swap_chunk_idx + .read() + .expect("failed to acquire read lock"); + assert!(chunk_idx[[0, 0, 0]]); + assert!(!chunk_idx[[1, 0, 0]]); + } + + let second_option_subset = ArraySubset::new_with_ranges(&[1..2, 0..2, 0..2]); + writer.ensure_derived_data_for_subset(&array, &second_option_subset); + + let chunk_idx = writer + .swap_chunk_idx + .read() + .expect("failed to acquire read lock"); + assert_eq!(*chunk_idx, Array3::from_elem((2, 1, 1), true)); + + let cost_band_0: Vec = Array::open(swap.clone(), "/cost") + .expect("could not open derived cost array") + .retrieve_array_subset_elements(&first_option_subset) + .expect("could not read first option cost array"); + let cost_band_1: Vec = Array::open(swap, "/cost") + .expect("could not open derived cost array") + .retrieve_array_subset_elements(&second_option_subset) + .expect("could not read second option cost array"); + + assert_eq!(cost_band_0, vec![1.0; 4]); + assert_eq!(cost_band_1, vec![2.0; 4]); } } diff --git a/crates/revrt/src/dataset/mod.rs b/crates/revrt/src/dataset/mod.rs index 24d68962..ea107e39 100644 --- a/crates/revrt/src/dataset/mod.rs +++ b/crates/revrt/src/dataset/mod.rs @@ -312,8 +312,10 @@ mod tests { #[test] fn test_simple_cost_function_get_3x3() { let tmp = samples::multi_variable_random(1, 8, 8, 1, 4, 4, &["A", "B", "C", "cost"]); - let cost_function = - CostFunction::from_json(r#"{"cost_layers": [{"layer_name": "A"}]}"#).unwrap(); + let cost_function = CostFunction::from_json( + r#"{"routing_options":{"default":{"cost_layers":[{"layer_name":"A"}]}}}"#, + ) + .unwrap(); let dataset = Dataset::open(tmp.path(), cost_function, 1_000).expect("Error opening dataset"); @@ -384,8 +386,10 @@ mod tests { .store_metadata() .unwrap(); - let cost_function = - CostFunction::from_json(r#"{"cost_layers": [{"layer_name": "A"}]}"#).unwrap(); + let cost_function = CostFunction::from_json( + r#"{"routing_options":{"default":{"cost_layers":[{"layer_name":"A"}]}}}"#, + ) + .unwrap(); let error = Dataset::open(tmp_path.path(), cost_function, 1_000) .err() @@ -405,7 +409,7 @@ mod tests { fn test_simple_invariant_cost_function_get_3x3() { let tmp = samples::multi_variable_random(1, 8, 8, 1, 4, 4, &["A", "B", "C", "cost"]); let cost_function = CostFunction::from_json( - r#"{"cost_layers": [{"layer_name": "A", "is_invariant": true}]}"#, + r#"{"routing_options":{"default":{"cost_layers":[{"layer_name":"A","is_invariant":true}]}}}"#, ) .unwrap(); let dataset = @@ -518,8 +522,10 @@ mod tests { #[test] fn test_get_3x3_single_item_array() { let tmp = samples::cost_as_index_zarr(1, 1, 1, 1, 1, 1); - let cost_function = - CostFunction::from_json(r#"{"cost_layers": [{"layer_name": "cost"}]}"#).unwrap(); + let cost_function = CostFunction::from_json( + r#"{"routing_options":{"default":{"cost_layers":[{"layer_name":"cost"}]}}}"#, + ) + .unwrap(); let dataset = Dataset::open(tmp.path(), cost_function, 1_000).expect("Error opening dataset"); @@ -541,8 +547,10 @@ mod tests { #[test_case((1, 1), vec![(0, 1, 2.), (1, 0, 2.5)] ; "bottom right corner")] fn test_get_3x3_two_by_two_array((si, sj): (u64, u64), expected_output: Vec<(u64, u64, f32)>) { let tmp = samples::cost_as_index_zarr(1, 2, 2, 1, 2, 2); - let cost_function = - CostFunction::from_json(r#"{"cost_layers": [{"layer_name": "cost"}]}"#).unwrap(); + let cost_function = CostFunction::from_json( + r#"{"routing_options":{"default":{"cost_layers":[{"layer_name":"cost"}]}}}"#, + ) + .unwrap(); let dataset = Dataset::open(tmp.path(), cost_function, 1_000).expect("Error opening dataset"); @@ -578,8 +586,10 @@ mod tests { expected_output: Vec<(u64, u64, f32)>, ) { let tmp = samples::cost_as_index_zarr(1, 3, 3, 1, 3, 3); - let cost_function = - CostFunction::from_json(r#"{"cost_layers": [{"layer_name": "cost"}]}"#).unwrap(); + let cost_function = CostFunction::from_json( + r#"{"routing_options":{"default":{"cost_layers":[{"layer_name":"cost"}]}}}"#, + ) + .unwrap(); let dataset = Dataset::open(tmp.path(), cost_function, 1_000).expect("Error opening dataset"); @@ -618,8 +628,10 @@ mod tests { expected_output: Vec<(u64, u64, f32)>, ) { let tmp = samples::cost_as_index_zarr(1, 4, 4, 1, 2, 2); - let cost_function = - CostFunction::from_json(r#"{"cost_layers": [{"layer_name": "cost"}]}"#).unwrap(); + let cost_function = CostFunction::from_json( + r#"{"routing_options":{"default":{"cost_layers":[{"layer_name":"cost"}]}}}"#, + ) + .unwrap(); let dataset = Dataset::open(tmp.path(), cost_function, 1_000).expect("Error opening dataset"); @@ -646,13 +658,17 @@ mod tests { // Define cost function: A normal, C invariant, friction from B * 0.5 let json = r#" { - "cost_layers": [ - {"layer_name": "A"}, - {"layer_name": "C", "is_invariant": true} - ], - "friction_layers": [ - {"multiplier_layer": "B", "multiplier_scalar": 0.5} - ] + "routing_options": { + "default": { + "cost_layers": [ + {"layer_name": "A"}, + {"layer_name": "C", "is_invariant": true} + ], + "friction_layers": [ + {"multiplier_layer": "B", "multiplier_scalar": 0.5} + ] + } + } } "#; @@ -734,8 +750,8 @@ mod tests { } } - #[test_case(r#"{"cost_layers": [{"layer_name": "B"}], "ignore_invalid_costs": true}"# ; "zero layer")] - #[test_case(r#"{"cost_layers": [{"layer_name": "C"}], "ignore_invalid_costs": true}"# ; "negative layer")] + #[test_case(r#"{"routing_options":{"default":{"cost_layers":[{"layer_name":"B"}]}},"ignore_invalid_costs":true}"# ; "zero layer")] + #[test_case(r#"{"routing_options":{"default":{"cost_layers":[{"layer_name":"C"}]}},"ignore_invalid_costs":true}"# ; "negative layer")] fn test_get_3x3_with_hard_barriered_layers(json: &str) { let tmp = samples::ZarrTestBuilder::new() .dimensions(1, 3, 3) @@ -756,8 +772,8 @@ mod tests { ); } - #[test_case(r#"{"cost_layers": [{"layer_name": "B"}], "ignore_invalid_costs": false}"# ; "zero layer")] - #[test_case(r#"{"cost_layers": [{"layer_name": "C"}], "ignore_invalid_costs": false}"# ; "negative layer")] + #[test_case(r#"{"routing_options":{"default":{"cost_layers":[{"layer_name":"B"}]}},"ignore_invalid_costs":false}"# ; "zero layer")] + #[test_case(r#"{"routing_options":{"default":{"cost_layers":[{"layer_name":"C"}]}},"ignore_invalid_costs":false}"# ; "negative layer")] fn test_get_3x3_with_soft_barrier_layers(json: &str) { let tmp = samples::ZarrTestBuilder::new() .dimensions(1, 3, 3) @@ -811,14 +827,18 @@ mod tests { fn test_get_3x3_keeps_explicit_barriers_out_of_cached_costs() { let json = r#" { - "cost_layers": [{"layer_name": "A"}], - "barrier_layers": [ - { - "layer_name": "B", - "barrier_operator": "eq", - "barrier_threshold": 1.0 + "routing_options": { + "default": { + "cost_layers": [{"layer_name": "A"}], + "barrier_layers": [ + { + "layer_name": "B", + "barrier_operator": "eq", + "barrier_threshold": 1.0 + } + ] } - ] + } } "#; @@ -852,15 +872,19 @@ mod tests { fn test_explicit_barriers_do_not_modify_cached_costs_when_invalid_costs_are_soft() { let json = r#" { - "cost_layers": [{"layer_name": "A"}], - "barrier_layers": [ - { - "layer_name": "B", - "barrier_operator": "eq", - "barrier_threshold": 1.0, - "barrier_importance": 1 + "routing_options": { + "default": { + "cost_layers": [{"layer_name": "A"}], + "barrier_layers": [ + { + "layer_name": "B", + "barrier_operator": "eq", + "barrier_threshold": 1.0, + "barrier_importance": 1 + } + ] } - ], + }, "ignore_invalid_costs": false } "#; @@ -887,21 +911,25 @@ mod tests { fn test_cumulative_soft_barrier_masks_follow_retry_state() { let json = r#" { - "cost_layers": [{"layer_name": "A"}], - "barrier_layers": [ - { - "layer_name": "B", - "barrier_operator": "eq", - "barrier_threshold": 1.0, - "barrier_importance": 1 - }, - { - "layer_name": "C", - "barrier_operator": "eq", - "barrier_threshold": 1.0, - "barrier_importance": 2 + "routing_options": { + "default": { + "cost_layers": [{"layer_name": "A"}], + "barrier_layers": [ + { + "layer_name": "B", + "barrier_operator": "eq", + "barrier_threshold": 1.0, + "barrier_importance": 1 + }, + { + "layer_name": "C", + "barrier_operator": "eq", + "barrier_threshold": 1.0, + "barrier_importance": 2 + } + ] } - ] + } } "#; @@ -941,21 +969,25 @@ mod tests { fn test_cumulative_soft_barrier_masks_or_tied_importance_groups() { let json = r#" { - "cost_layers": [{"layer_name": "A"}], - "barrier_layers": [ - { - "layer_name": "B", - "barrier_operator": "eq", - "barrier_threshold": 1.0, - "barrier_importance": 1 - }, - { - "layer_name": "C", - "barrier_operator": "eq", - "barrier_threshold": 1.0, - "barrier_importance": 1 + "routing_options": { + "default": { + "cost_layers": [{"layer_name": "A"}], + "barrier_layers": [ + { + "layer_name": "B", + "barrier_operator": "eq", + "barrier_threshold": 1.0, + "barrier_importance": 1 + }, + { + "layer_name": "C", + "barrier_operator": "eq", + "barrier_threshold": 1.0, + "barrier_importance": 1 + } + ] } - ] + } } "#; diff --git a/crates/revrt/src/dataset/reader.rs b/crates/revrt/src/dataset/reader.rs index 628cbeb0..3813d08c 100644 --- a/crates/revrt/src/dataset/reader.rs +++ b/crates/revrt/src/dataset/reader.rs @@ -695,12 +695,105 @@ mod tests { ); assert_eq!( retry_one, - vec![ArrayIndex { i: 0, j: 0 }, ArrayIndex { i: 2, j: 1 }] + vec![ArrayIndex::new_ij(0, 0), ArrayIndex::new_ij(2, 1)] ); } - fn reader_for_grid(grid_nrows: u64, grid_ncols: u64) -> NeighborhoodReader { + #[test] + fn get_3x3_reads_from_requested_option_band() { + let fixture = reader_fixture_with_shape( + 2, + 3, + 3, + vec![ + 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, + 17.0, 18.0, 19.0, + ], + vec![0.0; 18], + vec![false; 18], + vec![false; 18], + vec![false; 18], + ); + + let neighbors = fixture.reader.get_3x3( + &ArrayIndex { + i: 1, + j: 1, + option: 1, + }, + &NoOpMaterializer { + has_hard_barriers: false, + }, + ); + + assert!(neighbors.iter().all(|(index, _)| index.option == 1)); + assert!(neighbors.contains(&( + ArrayIndex { + i: 0, + j: 1, + option: 1 + }, + 13.5 + ))); + assert!(neighbors.contains(&( + ArrayIndex { + i: 1, + j: 2, + option: 1 + }, + 15.5 + ))); + } + + #[test] + fn grid_shape_reports_option_count() { + let fixture = reader_fixture_with_shape( + 2, + 3, + 4, + vec![1.0; 24], + vec![0.0; 24], + vec![false; 24], + vec![false; 24], + vec![false; 24], + ); + + assert_eq!(fixture.reader.grid_shape(), (3, 4, 2)); + } + + #[test] + fn get_cell_cost_reads_requested_option_band() { + let fixture = reader_fixture_with_shape( + 2, + 2, + 2, + vec![1.0, 2.0, 3.0, 4.0, 10.0, 20.0, 30.0, 40.0], + vec![0.5, 0.5, 0.5, 0.5, 1.0, 1.0, 1.0, 1.0], + vec![false; 8], + vec![false; 8], + vec![false; 8], + ); + + let cost = fixture + .reader + .get_cell_cost( + &ArrayIndex { + i: 1, + j: 0, + option: 1, + }, + &NoOpMaterializer { + has_hard_barriers: false, + }, + ) + .unwrap(); + + assert_eq!(cost, 31.0); + } + + fn reader_for_grid(grid_nrows: u64, grid_ncols: u64) -> DerivedDataReader { let fixture = reader_fixture_with_shape( + 1, grid_nrows, grid_ncols, vec![1.0; (grid_nrows * grid_ncols) as usize], @@ -720,6 +813,7 @@ mod tests { soft_retry_one_values: Vec, ) -> ReaderFixture { reader_fixture_with_shape( + 1, 3, 3, cost_values, @@ -730,7 +824,9 @@ mod tests { ) } + #[allow(clippy::too_many_arguments)] fn reader_fixture_with_shape( + grid_noptions: u64, grid_nrows: u64, grid_ncols: u64, cost_values: Vec, @@ -740,25 +836,33 @@ mod tests { soft_retry_one_values: Vec, ) -> ReaderFixture { let source_tmp = ZarrTestBuilder::new() - .dimensions(1, grid_nrows, grid_ncols) - .chunks(1, grid_nrows, grid_ncols) + .dimensions(grid_noptions, grid_nrows, grid_ncols) + .chunks(grid_noptions, grid_nrows, grid_ncols) .layer(LayerConfig::ones("source")) .build() .expect("failed to create source test dataset"); let source: ReadableListableStorage = Arc::new( FilesystemStore::new(source_tmp.path()).expect("could not open source test store"), ); - let layout = - inspect_source_layout(&source).expect("source layout inspection should succeed"); + let layout = inspect_source_layout(&source, grid_noptions as u32) + .expect("source layout inspection should succeed"); let swap_tmp = TempDir::new().expect("could not create temporary swap"); let swap = initialize_swap(swap_tmp.path(), &layout, 1) .expect("swap initialization should succeed"); - store_f32_layer(swap.clone(), "/cost", grid_nrows, grid_ncols, cost_values); + store_f32_layer( + swap.clone(), + "/cost", + grid_noptions, + grid_nrows, + grid_ncols, + cost_values, + ); store_f32_layer( swap.clone(), "/cost_invariant", + grid_noptions, grid_nrows, grid_ncols, invariant_values, @@ -766,6 +870,7 @@ mod tests { store_bool_layer( swap.clone(), "/hard_barrier_mask", + grid_noptions, grid_nrows, grid_ncols, hard_barrier_values, @@ -773,6 +878,7 @@ mod tests { store_bool_layer( swap.clone(), "/soft_barrier_mask_retry_0", + grid_noptions, grid_nrows, grid_ncols, soft_retry_zero_values, @@ -780,6 +886,7 @@ mod tests { store_bool_layer( swap.clone(), "/soft_barrier_mask_retry_1", + grid_noptions, grid_nrows, grid_ncols, soft_retry_one_values, @@ -797,13 +904,20 @@ mod tests { fn store_f32_layer( swap: ReadableWritableListableStorage, path: &str, + grid_noptions: u64, grid_nrows: u64, grid_ncols: u64, values: Vec, ) { - let data = - Array3::from_shape_vec((1_usize, grid_nrows as usize, grid_ncols as usize), values) - .expect("f32 layer values should match requested shape"); + let data = Array3::from_shape_vec( + ( + grid_noptions as usize, + grid_nrows as usize, + grid_ncols as usize, + ), + values, + ) + .expect("f32 layer values should match requested shape"); let array = Array::open(swap, path).expect("expected f32 layer to exist"); let subset = chunk_subset(&array); @@ -815,13 +929,20 @@ mod tests { fn store_bool_layer( swap: ReadableWritableListableStorage, path: &str, + grid_noptions: u64, grid_nrows: u64, grid_ncols: u64, values: Vec, ) { - let data = - Array3::from_shape_vec((1_usize, grid_nrows as usize, grid_ncols as usize), values) - .expect("bool layer values should match requested shape"); + let data = Array3::from_shape_vec( + ( + grid_noptions as usize, + grid_nrows as usize, + grid_ncols as usize, + ), + values, + ) + .expect("bool layer values should match requested shape"); let array = Array::open(swap, path).expect("expected bool layer to exist"); let subset = chunk_subset(&array); diff --git a/crates/revrt/src/lib.rs b/crates/revrt/src/lib.rs index e0a21dec..c09541f1 100644 --- a/crates/revrt/src/lib.rs +++ b/crates/revrt/src/lib.rs @@ -213,8 +213,10 @@ mod tests { algorithm: &str, ) { let store_path = dataset::samples::uniform_cost_zarr(1, 8, 8, 1, 4, 4, 1.0); - let cost_function = - CostFunction::from_json(r#"{"cost_layers": [{"layer_name": "cost"}]}"#).unwrap(); + let cost_function = CostFunction::from_json( + r#"{"routing_options":{"default":{"cost_layers":[{"layer_name":"cost"}]}}}"#, + ) + .unwrap(); let mut simulation = Routing::new(&store_path, cost_function, 1_000, algorithm).unwrap(); let start = vec![ArrayIndex::new_ij(si, sj)]; let end = vec![ArrayIndex::new_ij(ei, ej)]; @@ -239,8 +241,10 @@ mod tests { algorithm: &str, ) { let store_path = dataset::samples::uniform_cost_zarr(1, 8, 8, 1, 4, 4, 1.0); - let cost_function = - CostFunction::from_json(r#"{"cost_layers": [{"layer_name": "cost"}]}"#).unwrap(); + let cost_function = CostFunction::from_json( + r#"{"routing_options":{"default":{"cost_layers":[{"layer_name":"cost"}]}}}"#, + ) + .unwrap(); let mut simulation = Routing::new(&store_path, cost_function, 1_000, algorithm).unwrap(); let start = vec![ArrayIndex::new_ij(si, sj)]; let end = endpoints @@ -277,12 +281,16 @@ mod tests { let cost_function = CostFunction::from_json( r#"{ - "cost_layers": [{"layer_name": "cost"}], - "barrier_layers": [{ - "layer_name": "barrier", - "barrier_operator": "eq", - "barrier_threshold": 1.0 - }], + "routing_options": { + "default": { + "cost_layers": [{"layer_name": "cost"}], + "barrier_layers": [{ + "layer_name": "barrier", + "barrier_operator": "eq", + "barrier_threshold": 1.0 + }] + } + }, "ignore_invalid_costs": false }"#, ) @@ -318,8 +326,10 @@ mod tests { algorithm: &str, ) { let store_path = dataset::samples::uniform_cost_zarr(1, 8, 8, 1, 4, 4, cost_array_fill); - let cost_function = - CostFunction::from_json(r#"{"cost_layers": [{"layer_name": "cost"}]}"#).unwrap(); + let cost_function = CostFunction::from_json( + r#"{"routing_options":{"default":{"cost_layers":[{"layer_name":"cost"}]}}}"#, + ) + .unwrap(); let mut simulation = Routing::new(&store_path, cost_function, 1_000, algorithm).unwrap(); let start = vec![ArrayIndex::new_ij(si, sj)]; let end = endpoints @@ -349,8 +359,10 @@ mod tests { // Due to truncation solution to handle f32 costs. fn routing_many_to_many(algorithm: &str) { let store_path = dataset::samples::uniform_cost_zarr(1, 8, 8, 1, 4, 4, 1.0); - let cost_function = - CostFunction::from_json(r#"{"cost_layers": [{"layer_name": "cost"}]}"#).unwrap(); + let cost_function = CostFunction::from_json( + r#"{"routing_options":{"default":{"cost_layers":[{"layer_name":"cost"}]}}}"#, + ) + .unwrap(); let mut simulation = Routing::new(&store_path, cost_function, 1_000, algorithm).unwrap(); let start = vec![ ArrayIndex::new_ij(1, 1), @@ -385,8 +397,10 @@ mod tests { #[test_case("bidirectional-long-range-dijkstra"; "bidirectional-long-range")] fn routing_many_to_one(algorithm: &str) { let store_path = dataset::samples::uniform_cost_zarr(1, 8, 8, 1, 4, 4, 1.0); - let cost_function = - CostFunction::from_json(r#"{"cost_layers": [{"layer_name": "cost"}]}"#).unwrap(); + let cost_function = CostFunction::from_json( + r#"{"routing_options":{"default":{"cost_layers":[{"layer_name":"cost"}]}}}"#, + ) + .unwrap(); let mut simulation = Routing::new(&store_path, cost_function, 1_000, algorithm).unwrap(); let start = vec![ArrayIndex::new_ij(1, 1), ArrayIndex::new_ij(5, 5)]; let end = vec![ArrayIndex::new_ij(3, 3)]; @@ -424,8 +438,10 @@ mod tests { .build() .expect("failed to build zarr test dataset"); - let cost_function = - CostFunction::from_json(r#"{"cost_layers": [{"layer_name": "cost"}]}"#).unwrap(); + let cost_function = CostFunction::from_json( + r#"{"routing_options":{"default":{"cost_layers":[{"layer_name":"cost"}]}}}"#, + ) + .unwrap(); let mut simulation = Routing::new(&store_path, cost_function, 1_000, algorithm).unwrap(); let start = vec![ArrayIndex::new_ij(0, 0)]; diff --git a/crates/revrt/src/routing/astar.rs b/crates/revrt/src/routing/astar.rs index 8bdd259c..4c59c9be 100644 --- a/crates/revrt/src/routing/astar.rs +++ b/crates/revrt/src/routing/astar.rs @@ -121,4 +121,67 @@ mod tests { assert_eq!(neighbors.len(), 2); assert_eq!(min_cost.get(), Some(12)); } + + #[test] + fn layered_octile_distance_ignores_option_dimension() { + let goals = [ + ArrayIndex { + i: 3, + j: 3, + option: 1, + }, + ArrayIndex { + i: 8, + j: 8, + option: 2, + }, + ]; + let distance = super::octile_distance( + &ArrayIndex { + i: 0, + j: 1, + option: 0, + }, + &goals, + ); + + assert_eq!(distance, 1.0 + 2.0 * std::f64::consts::SQRT_2); + } + + #[test] + fn layered_astar_successors_tracks_lowest_seen_cost() { + let min_cost = Cell::new(None); + + let neighbors = super::astar_successors( + &ArrayIndex { + i: 1, + j: 1, + option: 0, + }, + &mut |_| { + vec![ + ( + ArrayIndex { + i: 1, + j: 2, + option: 0, + }, + 15_u64, + ), + ( + ArrayIndex { + i: 1, + j: 1, + option: 1, + }, + 8_u64, + ), + ] + }, + &min_cost, + ); + + assert_eq!(neighbors.len(), 2); + assert_eq!(min_cost.get(), Some(8)); + } } diff --git a/crates/revrt/src/routing/mod.rs b/crates/revrt/src/routing/mod.rs index 5c54caa8..fb882ba1 100644 --- a/crates/revrt/src/routing/mod.rs +++ b/crates/revrt/src/routing/mod.rs @@ -331,6 +331,10 @@ mod tests { use super::{Algorithm, AlgorithmType, Scenario, compute_route_attempt_result}; use crate::ArrayIndex; + fn layered_switch_cost(band: u64, _row: u64, _col: u64) -> f32 { + if band == 0 { 1.0 } else { 4.0 } + } + #[test] fn compute_route_attempt_result_tracks_dropped_barriers_per_start() { let store = crate::dataset::samples::ZarrTestBuilder::new() @@ -353,20 +357,24 @@ mod tests { .unwrap(); let cost_function = crate::cost::CostFunction::from_json( r#"{ - "cost_layers": [{"layer_name": "cost"}], - "barrier_layers": [ - { - "layer_name": "hard_barrier", - "barrier_operator": "eq", - "barrier_threshold": 1.0 - }, - { - "layer_name": "soft_barrier", - "barrier_operator": "eq", - "barrier_threshold": 1.0, - "barrier_importance": 1 + "routing_options": { + "default": { + "cost_layers": [{"layer_name": "cost"}], + "barrier_layers": [ + { + "layer_name": "hard_barrier", + "barrier_operator": "eq", + "barrier_threshold": 1.0 + }, + { + "layer_name": "soft_barrier", + "barrier_operator": "eq", + "barrier_threshold": 1.0, + "barrier_importance": 1 + } + ] } - ], + }, "ignore_invalid_costs": false }"#, ) @@ -400,4 +408,266 @@ mod tests { ); assert!(bottom_solution.dropped_barrier_layers().is_empty()); } + + #[test] + fn compute_route_attempt_result_can_switch_options_in_place() { + let store = crate::dataset::samples::ZarrTestBuilder::new() + .dimensions(2, 1, 1) + .chunks(2, 1, 1) + .layer(crate::dataset::samples::LayerConfig::custom( + "cost", + layered_switch_cost, + )) + .build() + .unwrap(); + let cost_function = crate::cost::CostFunction::from_json( + r#"{ + "routing_options": { + "overhead": { + "cost_layers": [{"layer_name": "cost"}] + }, + "underground": { + "cost_layers": [{"layer_name": "cost"}] + } + }, + "ignore_invalid_costs": false + }"#, + ) + .unwrap(); + let scenario = Scenario::new(store.path(), cost_function, 1_000).unwrap(); + let algorithm = Algorithm::from_selection(AlgorithmType::Dijkstra, 8 * 1024 * 1024); + let start = [ArrayIndex { + i: 0, + j: 0, + option: 0, + }]; + let end = [ArrayIndex { + i: 0, + j: 0, + option: 1, + }]; + + let result = compute_route_attempt_result(&scenario, &algorithm, &start, &end); + + assert_eq!(result.len(), 1); + assert_eq!( + result[0].route(), + &vec![ + ArrayIndex { + i: 0, + j: 0, + option: 0 + }, + ArrayIndex { + i: 0, + j: 0, + option: 1 + }, + ] + ); + assert_eq!(*result[0].total_cost(), 4.0); + } + + #[test] + fn compute_route_attempt_result_applies_transition_costs() { + let store = crate::dataset::samples::ZarrTestBuilder::new() + .dimensions(2, 1, 1) + .chunks(2, 1, 1) + .layer(crate::dataset::samples::LayerConfig::custom( + "cost", + layered_switch_cost, + )) + .build() + .unwrap(); + let cost_function = crate::cost::CostFunction::from_json( + r#"{ + "routing_options": { + "overhead": { + "cost_layers": [{"layer_name": "cost"}] + }, + "underground": { + "cost_layers": [{"layer_name": "cost"}] + } + }, + "transition_costs": { + "pairwise": [ + { + "from": "overhead", + "to": "underground", + "cost": 3.0 + } + ] + }, + "ignore_invalid_costs": false + }"#, + ) + .unwrap(); + let scenario = Scenario::new(store.path(), cost_function, 1_000).unwrap(); + let algorithm = Algorithm::from_selection(AlgorithmType::Dijkstra, 8 * 1024 * 1024); + let start = [ArrayIndex { + i: 0, + j: 0, + option: 0, + }]; + let end = [ArrayIndex { + i: 0, + j: 0, + option: 1, + }]; + + let result = compute_route_attempt_result(&scenario, &algorithm, &start, &end); + + assert_eq!(result.len(), 1); + assert_eq!(*result[0].total_cost(), 7.0); + } + + // fn any_option_start_cost(band: u64, _row: u64, _col: u64) -> f32 { + // if band == 0 { 1.0 } else { 2.0 } + // } + + // fn first_option_blocked_everywhere(band: u64, _row: u64, _col: u64) -> f32 { + // if band == 0 { 1.0 } else { 0.0 } + // } + + // #[test] + // fn compute_route_attempt_result_can_start_on_any_allowed_option() { + // let store = crate::dataset::samples::ZarrTestBuilder::new() + // .dimensions(2, 1, 2) + // .chunks(2, 1, 2) + // .layer(crate::dataset::samples::LayerConfig::custom( + // "cost", + // any_option_start_cost, + // )) + // .layer(crate::dataset::samples::LayerConfig::custom( + // "hard_barrier", + // first_option_blocked_everywhere, + // )) + // .build() + // .unwrap(); + // let cost_function = crate::cost::CostFunction::from_json( + // r#"{ + // "routing_options": { + // "overhead": { + // "cost_layers": [{"layer_name": "cost"}], + // "barrier_layers": [ + // { + // "layer_name": "hard_barrier", + // "barrier_operator": "eq", + // "barrier_threshold": 1.0 + // } + // ] + // }, + // "underground": { + // "cost_layers": [{"layer_name": "cost"}], + // "barrier_layers": [ + // { + // "layer_name": "hard_barrier", + // "barrier_operator": "eq", + // "barrier_threshold": 1.0 + // } + // ] + // } + // }, + // "ignore_invalid_costs": false + // }"#, + // ) + // .unwrap(); + // let scenario = Scenario::new(store.path(), cost_function, 1_000).unwrap(); + // let algorithm = Algorithm::from_selection(AlgorithmType::Dijkstra, 8 * 1024 * 1024); + // let start = [ArrayIndex::new_ij(0, 0)]; + // let end = [ArrayIndex::new_ij(0, 1)]; + + // let result = compute_route_attempt_result(&scenario, &algorithm, &start, &end); + + // assert_eq!(result.len(), 1); + // assert_eq!( + // result[0].route(), + // &vec![ + // ArrayIndex { + // i: 0, + // j: 0, + // option: 1 + // }, + // ArrayIndex { + // i: 0, + // j: 1, + // option: 1 + // }, + // ] + // ); + // assert_eq!(*result[0].total_cost(), 2.0); + // } + + // #[test] + // fn compute_route_attempt_result_can_end_on_any_allowed_option() { + // let store = crate::dataset::samples::ZarrTestBuilder::new() + // .dimensions(2, 1, 2) + // .chunks(2, 1, 2) + // .layer(crate::dataset::samples::LayerConfig::custom( + // "cost", + // any_option_start_cost, + // )) + // .layer(crate::dataset::samples::LayerConfig::custom( + // "hard_barrier", + // first_option_blocked_everywhere, + // )) + // .build() + // .unwrap(); + // let cost_function = crate::cost::CostFunction::from_json( + // r#"{ + // "routing_options": { + // "overhead": { + // "cost_layers": [{"layer_name": "cost"}], + // "barrier_layers": [ + // { + // "layer_name": "hard_barrier", + // "barrier_operator": "eq", + // "barrier_threshold": 1.0 + // } + // ] + // }, + // "underground": { + // "cost_layers": [{"layer_name": "cost"}], + // "barrier_layers": [ + // { + // "layer_name": "hard_barrier", + // "barrier_operator": "eq", + // "barrier_threshold": 1.0 + // } + // ] + // } + // }, + // "ignore_invalid_costs": false + // }"#, + // ) + // .unwrap(); + // let scenario = Scenario::new(store.path(), cost_function, 1_000).unwrap(); + // let algorithm = Algorithm::from_selection(AlgorithmType::Dijkstra, 8 * 1024 * 1024); + // let start = [ArrayIndex { + // i: 0, + // j: 0, + // option: 1, + // }]; + // let end = [ArrayIndex::new_ij(0, 1)]; + + // let result = compute_route_attempt_result(&scenario, &algorithm, &start, &end); + + // assert_eq!(result.len(), 1); + // assert_eq!( + // result[0].route(), + // &vec![ + // ArrayIndex { + // i: 0, + // j: 0, + // option: 1 + // }, + // ArrayIndex { + // i: 0, + // j: 1, + // option: 1 + // }, + // ] + // ); + // assert_eq!(*result[0].total_cost(), 2.0); + // } } diff --git a/crates/revrt/src/routing/scenario.rs b/crates/revrt/src/routing/scenario.rs index 98df0399..7a8d2ea3 100644 --- a/crates/revrt/src/routing/scenario.rs +++ b/crates/revrt/src/routing/scenario.rs @@ -307,6 +307,30 @@ mod tests { use super::Scenario; use crate::ArrayIndex; + fn option_cost(band: u64, _row: u64, _col: u64) -> f32 { + if band == 0 { 1.0 } else { 5.0 } + } + + fn second_option_center_barrier(band: u64, row: u64, col: u64) -> f32 { + if band == 1 && row == 1 && col == 1 { + 1.0 + } else { + 0.0 + } + } + + fn option_zone(row: u64, col: u64) -> f32 { + if row == 1 && col == 1 { 1.0 } else { 0.0 } + } + + fn overhead_option_cost(band: u64, _row: u64, _col: u64) -> f32 { + if band == 0 { 1.0 } else { 9.0 } + } + + fn underground_option_cost(band: u64, _row: u64, _col: u64) -> f32 { + if band == 1 { 5.0 } else { 8.0 } + } + #[test] fn successors_keep_hard_barriers_after_soft_groups_drop() { let store = crate::dataset::samples::ZarrTestBuilder::new() @@ -329,20 +353,24 @@ mod tests { .unwrap(); let cost_function = crate::cost::CostFunction::from_json( r#"{ - "cost_layers": [{"layer_name": "cost"}], - "barrier_layers": [ - { - "layer_name": "hard_barrier", - "barrier_operator": "eq", - "barrier_threshold": 1.0 - }, - { - "layer_name": "soft_barrier", - "barrier_operator": "eq", - "barrier_threshold": 1.0, - "barrier_importance": 1 + "routing_options": { + "default": { + "cost_layers": [{"layer_name": "cost"}], + "barrier_layers": [ + { + "layer_name": "hard_barrier", + "barrier_operator": "eq", + "barrier_threshold": 1.0 + }, + { + "layer_name": "soft_barrier", + "barrier_operator": "eq", + "barrier_threshold": 1.0, + "barrier_importance": 1 + } + ] } - ], + }, "ignore_invalid_costs": false }"#, ) @@ -392,14 +420,18 @@ mod tests { .unwrap(); let cost_function = crate::cost::CostFunction::from_json( r#"{ - "cost_layers": [{"layer_name": "cost"}], - "barrier_layers": [ - { - "layer_name": "hard_barrier", - "barrier_operator": "eq", - "barrier_threshold": 1.0 + "routing_options": { + "default": { + "cost_layers": [{"layer_name": "cost"}], + "barrier_layers": [ + { + "layer_name": "hard_barrier", + "barrier_operator": "eq", + "barrier_threshold": 1.0 + } + ] } - ], + }, "ignore_invalid_costs": false }"#, ) @@ -411,6 +443,371 @@ mod tests { assert!(successors.is_empty()); } + #[test] + fn successors_include_same_pixel_option_transitions() { + let store = crate::dataset::samples::ZarrTestBuilder::new() + .dimensions(2, 3, 3) + .chunks(2, 3, 3) + .layer(crate::dataset::samples::LayerConfig::custom( + "cost", + option_cost, + )) + .build() + .unwrap(); + let cost_function = crate::cost::CostFunction::from_json( + r#"{ + "routing_options": { + "overhead": { + "cost_layers": [{"layer_name": "cost"}] + }, + "underground": { + "cost_layers": [{"layer_name": "cost"}] + } + }, + "ignore_invalid_costs": false + }"#, + ) + .unwrap(); + let scenario = Scenario::new(store.path(), cost_function, 1_000).unwrap(); + + let successors = scenario.successors_for_attempt( + &ArrayIndex { + i: 1, + j: 1, + option: 0, + }, + 0, + ); + + assert!(successors.iter().any(|(index, cost)| { + *index + == ArrayIndex { + i: 1, + j: 1, + option: 1, + } + && *cost == 50_000 + })); + } + + #[test] + fn successors_skip_same_pixel_transitions_into_blocked_options() { + let store = crate::dataset::samples::ZarrTestBuilder::new() + .dimensions(2, 3, 3) + .chunks(2, 3, 3) + .layer(crate::dataset::samples::LayerConfig::custom( + "cost", + option_cost, + )) + .layer(crate::dataset::samples::LayerConfig::custom( + "hard_barrier", + second_option_center_barrier, + )) + .build() + .unwrap(); + let cost_function = crate::cost::CostFunction::from_json( + r#"{ + "routing_options": { + "overhead": { + "cost_layers": [{"layer_name": "cost"}], + "barrier_layers": [ + { + "layer_name": "hard_barrier", + "barrier_operator": "eq", + "barrier_threshold": 1.0 + } + ] + }, + "underground": { + "cost_layers": [{"layer_name": "cost"}], + "barrier_layers": [ + { + "layer_name": "hard_barrier", + "barrier_operator": "eq", + "barrier_threshold": 1.0 + } + ] + } + }, + "ignore_invalid_costs": false + }"#, + ) + .unwrap(); + let scenario = Scenario::new(store.path(), cost_function, 1_000).unwrap(); + + let successors = scenario.successors_for_attempt( + &ArrayIndex { + i: 1, + j: 1, + option: 0, + }, + 0, + ); + + assert!(!successors.iter().any(|(index, _)| *index + == ArrayIndex { + i: 1, + j: 1, + option: 1 + })); + } + + #[test] + fn successors_apply_configured_transition_costs() { + let store = crate::dataset::samples::ZarrTestBuilder::new() + .dimensions(2, 3, 3) + .chunks(2, 3, 3) + .layer(crate::dataset::samples::LayerConfig::custom( + "cost", + option_cost, + )) + .build() + .unwrap(); + let cost_function = crate::cost::CostFunction::from_json( + r#"{ + "routing_options": { + "overhead": { + "cost_layers": [{"layer_name": "cost"}] + }, + "underground": { + "cost_layers": [{"layer_name": "cost"}] + } + }, + "transition_costs": { + "default": 1.0, + "pairwise": [ + { + "from": "overhead", + "to": "underground", + "cost": 3.0, + "applies_bidirectionally": true + } + ] + }, + "ignore_invalid_costs": false + }"#, + ) + .unwrap(); + let scenario = Scenario::new(store.path(), cost_function, 1_000).unwrap(); + + let successors = scenario.successors_for_attempt( + &ArrayIndex { + i: 1, + j: 1, + option: 0, + }, + 0, + ); + + assert!(successors.iter().any(|(index, cost)| { + *index + == ArrayIndex { + i: 1, + j: 1, + option: 1, + } + && *cost == 80_000 + })); + } + + #[test] + fn successors_apply_transition_costs_from_object_routing_options() { + let store = crate::dataset::samples::ZarrTestBuilder::new() + .dimensions(2, 3, 3) + .chunks(2, 3, 3) + .layer(crate::dataset::samples::LayerConfig::custom( + "overhead_cost", + overhead_option_cost, + )) + .layer(crate::dataset::samples::LayerConfig::custom( + "underground_cost", + underground_option_cost, + )) + .build() + .unwrap(); + let cost_function = crate::cost::CostFunction::from_json( + r#"{ + "routing_options": { + "overhead": { + "cost_layers": [{"layer_name": "overhead_cost"}] + }, + "underground": { + "cost_layers": [{"layer_name": "underground_cost"}] + } + }, + "transition_costs": { + "pairwise": [ + { + "from": "overhead", + "to": "underground", + "cost": 3.0, + "applies_bidirectionally": true + } + ] + }, + "ignore_invalid_costs": false + }"#, + ) + .unwrap(); + let scenario = Scenario::new(store.path(), cost_function, 1_000).unwrap(); + + let successors = scenario.successors_for_attempt( + &ArrayIndex { + i: 1, + j: 1, + option: 0, + }, + 0, + ); + + assert!(successors.iter().any(|(index, cost)| { + *index + == ArrayIndex { + i: 1, + j: 1, + option: 1, + } + && *cost == 80_000 + })); + } + + #[test] + fn allowed_states_respect_driver_exclusions() { + let store = crate::dataset::samples::ZarrTestBuilder::new() + .dimensions(2, 3, 3) + .chunks(2, 3, 3) + .layer(crate::dataset::samples::LayerConfig::custom( + "cost", + option_cost, + )) + .layer(crate::dataset::samples::LayerConfig::custom( + "zone", + |_band, row, col| option_zone(row, col), + )) + .build() + .unwrap(); + let cost_function = crate::cost::CostFunction::from_json( + r#"{ + "routing_options": { + "overhead": { + "cost_layers": [{"layer_name": "cost"}] + }, + "underground": { + "cost_layers": [{"layer_name": "cost"}] + } + }, + "drivers": { + "default": { + "overhead": 1, + "underground": "excluded" + }, + "zones": [ + { + "layer_name": "zone", + "mask_operator": "eq", + "mask_threshold": 1, + "overhead": "excluded", + "underground": 1 + } + ] + }, + "ignore_invalid_costs": false + }"#, + ) + .unwrap(); + let scenario = Scenario::new(store.path(), cost_function, 1_000).unwrap(); + + let outside_zone = scenario + .allowed_states_at(&ArrayIndex::new_ij(0, 0), 0) + .collect::>(); + let inside_zone = scenario + .allowed_states_at(&ArrayIndex::new_ij(1, 1), 0) + .collect::>(); + + assert_eq!( + outside_zone, + vec![ArrayIndex { + i: 0, + j: 0, + option: 0 + }] + ); + assert_eq!( + inside_zone, + vec![ArrayIndex { + i: 1, + j: 1, + option: 1 + }] + ); + } + + #[test] + fn successors_apply_driver_multipliers() { + let store = crate::dataset::samples::ZarrTestBuilder::new() + .dimensions(2, 3, 3) + .chunks(2, 3, 3) + .layer(crate::dataset::samples::LayerConfig::custom( + "cost", + option_cost, + )) + .layer(crate::dataset::samples::LayerConfig::custom( + "zone", + |_band, row, col| option_zone(row, col), + )) + .build() + .unwrap(); + let cost_function = crate::cost::CostFunction::from_json( + r#"{ + "routing_options": { + "overhead": { + "cost_layers": [{"layer_name": "cost"}] + }, + "underground": { + "cost_layers": [{"layer_name": "cost"}] + } + }, + "drivers": { + "default": { + "overhead": 1, + "underground": 1 + }, + "zones": [ + { + "layer_name": "zone", + "mask_operator": "eq", + "mask_threshold": 1, + "overhead": 10, + "underground": 1 + } + ] + }, + "ignore_invalid_costs": false + }"#, + ) + .unwrap(); + let scenario = Scenario::new(store.path(), cost_function, 1_000).unwrap(); + + let successors = scenario.successors_for_attempt( + &ArrayIndex { + i: 0, + j: 1, + option: 0, + }, + 0, + ); + + assert!(successors.iter().any(|(index, cost)| { + *index + == ArrayIndex { + i: 1, + j: 1, + option: 0, + } + && *cost == 100_000 + })); + } + #[test] fn successors_use_cumulative_soft_masks_by_retry_state() { let store = crate::dataset::samples::ZarrTestBuilder::new() @@ -433,21 +830,25 @@ mod tests { .unwrap(); let cost_function = crate::cost::CostFunction::from_json( r#"{ - "cost_layers": [{"layer_name": "cost"}], - "barrier_layers": [ - { - "layer_name": "soft_barrier_low", - "barrier_operator": "eq", - "barrier_threshold": 1.0, - "barrier_importance": 1 - }, - { - "layer_name": "soft_barrier_high", - "barrier_operator": "eq", - "barrier_threshold": 1.0, - "barrier_importance": 2 + "routing_options": { + "default": { + "cost_layers": [{"layer_name": "cost"}], + "barrier_layers": [ + { + "layer_name": "soft_barrier_low", + "barrier_operator": "eq", + "barrier_threshold": 1.0, + "barrier_importance": 1 + }, + { + "layer_name": "soft_barrier_high", + "barrier_operator": "eq", + "barrier_threshold": 1.0, + "barrier_importance": 2 + } + ] } - ], + }, "ignore_invalid_costs": false }"#, ) @@ -508,15 +909,19 @@ mod tests { .unwrap(); let cost_function = crate::cost::CostFunction::from_json( r#"{ - "cost_layers": [{"layer_name": "cost"}], - "barrier_layers": [ - { - "layer_name": "soft_barrier", - "barrier_operator": "eq", - "barrier_threshold": 1.0, - "barrier_importance": 1 + "routing_options": { + "default": { + "cost_layers": [{"layer_name": "cost"}], + "barrier_layers": [ + { + "layer_name": "soft_barrier", + "barrier_operator": "eq", + "barrier_threshold": 1.0, + "barrier_importance": 1 + } + ] } - ], + }, "ignore_invalid_costs": false }"#, ) @@ -557,27 +962,31 @@ mod tests { .unwrap(); let cost_function = crate::cost::CostFunction::from_json( r#"{ - "cost_layers": [{"layer_name": "cost"}], - "barrier_layers": [ - { - "layer_name": "soft_barrier_low_a", - "barrier_operator": "eq", - "barrier_threshold": 1.0, - "barrier_importance": 1 - }, - { - "layer_name": "soft_barrier_low_b", - "barrier_operator": "eq", - "barrier_threshold": 1.0, - "barrier_importance": 1 - }, - { - "layer_name": "soft_barrier_high", - "barrier_operator": "eq", - "barrier_threshold": 1.0, - "barrier_importance": 2 + "routing_options": { + "default": { + "cost_layers": [{"layer_name": "cost"}], + "barrier_layers": [ + { + "layer_name": "soft_barrier_low_a", + "barrier_operator": "eq", + "barrier_threshold": 1.0, + "barrier_importance": 1 + }, + { + "layer_name": "soft_barrier_low_b", + "barrier_operator": "eq", + "barrier_threshold": 1.0, + "barrier_importance": 1 + }, + { + "layer_name": "soft_barrier_high", + "barrier_operator": "eq", + "barrier_threshold": 1.0, + "barrier_importance": 2 + } + ] } - ], + }, "ignore_invalid_costs": false }"#, ) From 37e58642e5b5555399861287d6d6f043a703ab2e Mon Sep 17 00:00:00 2001 From: Paul Date: Sat, 16 May 2026 16:40:38 -0600 Subject: [PATCH 23/40] Add feature --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 76c8afa2..c4b90e02 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,7 +36,7 @@ pyo3-build-config = "0.28.2" rand = "0.10.1" rayon = "1.11.0" serde = { version = "1.0.226", features = ["derive"] } -serde_json = { version = "1.0.143" } +serde_json = { version = "1.0.143", features = ["preserve_order"] } tempfile = "3.23.0" thiserror = "2.0.16" tokio = { version = "1.49.0", features = ["fs", "rt-multi-thread"] } From 0d3360cbe1861be1b0bd1a60485db0b4371a5e77 Mon Sep 17 00:00:00 2001 From: Paul Date: Tue, 19 May 2026 01:11:44 -0600 Subject: [PATCH 24/40] minor bug fix --- revrt/costs/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/revrt/costs/cli.py b/revrt/costs/cli.py index ae999705..f97d3db3 100644 --- a/revrt/costs/cli.py +++ b/revrt/costs/cli.py @@ -357,7 +357,7 @@ def _should_skip_layer(lf_handler, lc): logger.debug( "Existing config:\n%r\nNew config:\n%r", existing_build_config, - serialize_layer_build_dict, + serialized_build_config, ) logger.debug( "Existing cpm:\n%r\nNew cpm:\n%r", cpm, lc.values_are_costs_per_mile From aadc274e56a5a59d7164f711e9856eab8c25a2d4 Mon Sep 17 00:00:00 2001 From: Paul Date: Tue, 19 May 2026 01:11:52 -0600 Subject: [PATCH 25/40] Minor docstring --- revrt/utilities/handlers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/revrt/utilities/handlers.py b/revrt/utilities/handlers.py index b9ea97c4..7fd3a326 100644 --- a/revrt/utilities/handlers.py +++ b/revrt/utilities/handlers.py @@ -262,7 +262,8 @@ def layer_attrs(self, layer): Returns ------- dict - _description_ + Dictionary of attribute metadata for the requested layer + as stored in the Zarr variable attrs. Raises ------ From db43375b94a7fe9c3260788c4390d4aca6334263 Mon Sep 17 00:00:00 2001 From: Paul Date: Mon, 1 Jun 2026 14:56:06 -0600 Subject: [PATCH 26/40] Add inputs file --- crates/revrt/src/cost_new/inputs.rs | 230 ++++++++++++++++++++++++++++ 1 file changed, 230 insertions(+) create mode 100644 crates/revrt/src/cost_new/inputs.rs diff --git a/crates/revrt/src/cost_new/inputs.rs b/crates/revrt/src/cost_new/inputs.rs new file mode 100644 index 00000000..3499459d --- /dev/null +++ b/crates/revrt/src/cost_new/inputs.rs @@ -0,0 +1,230 @@ +//! Cost function inputs and parsing + +use serde::de::DeserializeOwned; +use serde_json::{Map, Value}; +use std::collections::HashMap; + +use crate::cost::{BarrierLayer, BarrierOperator, CostFunction, CostLayer, FrictionLayer}; +use crate::error::Result; + +fn true_option() -> bool { + true +} + +#[derive(Clone, Debug, serde::Deserialize)] +pub(crate) struct CostFunctionInput { + #[serde(default)] + routing_options: RoutingOptionsInput, + #[serde(default)] + drivers: DriversConfig, + #[serde(default)] + transition_costs: TransitionCostsConfig, + #[serde(default = "true_option")] + ignore_invalid_costs: bool, +} + +#[derive(Clone, Debug, Default, serde::Deserialize)] +#[serde(untagged)] +pub(crate) enum RoutingOptionsInput { + Definitions(Map), + #[default] + Missing, +} + +#[derive(Clone, Debug)] +pub(crate) struct RoutingOptionEntry { + pub(crate) name: String, + pub(crate) index: u32, + pub(crate) definition: TDefinition, +} + +#[derive(Clone, Debug, Default, serde::Deserialize)] +pub(crate) struct RoutingOptionDefinition { + #[serde(default)] + cost_layers: Vec, + #[serde(default)] + friction_layers: Vec, + #[serde(default)] + barrier_layers: Vec, +} + +#[derive(Clone, Debug, Default)] +pub(crate) struct RoutingOptionLayerSet { + pub(crate) cost_layers: Vec, + pub(crate) friction_layers: Vec, + pub(crate) barrier_layers: Vec, +} + +#[derive(Clone, Debug, serde::Deserialize)] +struct FrictionLayerInput { + #[serde(default)] + layer_name: Option, + #[serde(default)] + multiplier_layer: Option, + #[serde(default)] + multiplier_scalar: Option, +} + +#[derive(Clone, Debug, Default, serde::Deserialize)] +pub(crate) struct TransitionCostsConfig { + #[serde(default)] + pub(crate) default: f32, + #[serde(default)] + pub(crate) pairwise: Vec, +} + +#[derive(Clone, Debug, Default, serde::Deserialize)] +pub(crate) struct DriversConfig { + #[serde(default)] + pub(crate) default: HashMap, + #[serde(default)] + pub(crate) zones: Vec, +} + +#[derive(Clone, Debug, serde::Deserialize)] +pub(crate) struct DriverZoneConfig { + pub(crate) layer_name: String, + pub(crate) mask_operator: BarrierOperator, + pub(crate) mask_threshold: f32, + #[serde(flatten)] + pub(crate) options: HashMap, +} + +#[derive(Clone, Debug, serde::Deserialize)] +#[serde(untagged)] +pub(crate) enum DriverRuleValue { + Keyword(String), + Multiplier(f32), +} + +#[derive(Clone, Debug, serde::Deserialize)] +pub(crate) struct TransitionCostRule { + pub(crate) from: TransitionOptionRef, + pub(crate) to: TransitionOptionRef, + pub(crate) cost: f32, + #[serde(default)] + pub(crate) applies_bidirectionally: bool, +} + +#[derive(Clone, Debug, serde::Deserialize)] +#[serde(untagged)] +pub(crate) enum TransitionOptionRef { + Index(u32), + Name(String), +} + +impl RoutingOptionsInput { + pub(crate) fn into_entries(self) -> Result>> + where + TDefinition: DeserializeOwned, + { + match self { + Self::Definitions(definitions) => { + if definitions.is_empty() { + return Err(crate::error::Error::Undefined( + "routing_options must define at least one routing option".to_string(), + )); + } + + let mut entries = Vec::with_capacity(definitions.len()); + for (name, value) in definitions { + let definition = serde_json::from_value(value) + .map_err(|error| crate::error::Error::Undefined(error.to_string()))?; + entries.push(RoutingOptionEntry { + name, + index: entries.len() as u32, + definition, + }); + } + + Ok(entries) + } + Self::Missing => Err(crate::error::Error::Undefined( + "routing_options must be provided and all layer definitions must be nested under a routing option" + .to_string(), + )), + } + } +} + +impl TryFrom for CostFunction { + type Error = crate::error::Error; + + fn try_from(input: CostFunctionInput) -> Result { + let CostFunctionInput { + routing_options, + drivers, + transition_costs, + ignore_invalid_costs, + } = input; + let mut cost_layers = Vec::new(); + let mut friction_layers = Vec::new(); + let mut barrier_layers = Vec::new(); + let mut routing_option_names = Vec::new(); + + for RoutingOptionEntry { + name, + index, + definition, + } in routing_options.into_entries::()? + { + let RoutingOptionLayerSet { + cost_layers: option_cost_layers, + friction_layers: option_friction_layers, + barrier_layers: option_barrier_layers, + } = definition.into_layers(index)?; + routing_option_names.push(name); + cost_layers.extend(option_cost_layers); + friction_layers.extend(option_friction_layers); + barrier_layers.extend(option_barrier_layers); + } + + Ok(CostFunction::from_input_parts( + cost_layers, + friction_layers, + barrier_layers, + routing_option_names, + drivers, + transition_costs, + ignore_invalid_costs, + )) + } +} + +impl RoutingOptionDefinition { + pub(crate) fn into_layers(self, option: u32) -> Result { + Ok(RoutingOptionLayerSet { + cost_layers: self + .cost_layers + .into_iter() + .map(|layer| layer.with_option(option)) + .collect(), + friction_layers: self + .friction_layers + .into_iter() + .map(|layer| layer.into_layer(option)) + .collect::>()?, + barrier_layers: self + .barrier_layers + .into_iter() + .map(|layer| layer.with_option(option)) + .collect(), + }) + } +} + +impl FrictionLayerInput { + fn into_layer(self, option: u32) -> Result { + let multiplier_layer = self.layer_name.or(self.multiplier_layer).ok_or_else(|| { + crate::error::Error::Undefined( + "friction layer requires layer_name or multiplier_layer".to_string(), + ) + })?; + + Ok(FrictionLayer::new( + multiplier_layer, + self.multiplier_scalar, + option, + )) + } +} From fb01cea52795ffc355134782ca4e8d9621f22c2d Mon Sep 17 00:00:00 2001 From: Paul Date: Mon, 1 Jun 2026 15:42:20 -0600 Subject: [PATCH 27/40] Formatting --- crates/cli/src/main.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 063baa58..24b89dc0 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -58,7 +58,10 @@ fn main() { trace!("Starting point: {:?}", start); assert_eq!(cli.end.len(), 2); - let end = vec![revrt::ArrayIndex::new_ij(cli.end[0] as u64, cli.end[1] as u64)]; + let end = vec![revrt::ArrayIndex::new_ij( + cli.end[0] as u64, + cli.end[1] as u64, + )]; trace!("Ending point: {:?}", end); let result = resolve( From b85b1437b136006bd0750cee2dc40dd5b36759dc Mon Sep 17 00:00:00 2001 From: Paul Date: Mon, 1 Jun 2026 16:18:35 -0600 Subject: [PATCH 28/40] input now in charge of creating the driver rule set --- crates/revrt/src/cost_new/inputs.rs | 57 ++++++++++++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/crates/revrt/src/cost_new/inputs.rs b/crates/revrt/src/cost_new/inputs.rs index 3499459d..e19ec199 100644 --- a/crates/revrt/src/cost_new/inputs.rs +++ b/crates/revrt/src/cost_new/inputs.rs @@ -4,7 +4,10 @@ use serde::de::DeserializeOwned; use serde_json::{Map, Value}; use std::collections::HashMap; -use crate::cost::{BarrierLayer, BarrierOperator, CostFunction, CostLayer, FrictionLayer}; +use crate::cost::{ + BarrierLayer, BarrierOperator, CostFunction, CostLayer, DriverRuleSet, DriverZoneRule, + FrictionLayer, +}; use crate::error::Result; fn true_option() -> bool { @@ -179,6 +182,8 @@ impl TryFrom for CostFunction { barrier_layers.extend(option_barrier_layers); } + let drivers = drivers.into_rule_set(&routing_option_names)?; + Ok(CostFunction::from_input_parts( cost_layers, friction_layers, @@ -213,6 +218,36 @@ impl RoutingOptionDefinition { } } +impl DriversConfig { + pub(crate) fn into_rule_set(self, routing_options: &[String]) -> Result { + let mut default = vec![Some(1.0); routing_options.len()]; + + for (name, value) in self.default { + let option = resolve_routing_option(&name, routing_options, "drivers.default")?; + default[option as usize] = resolve_driver_rule_value(&value)?; + } + + let mut zones = Vec::with_capacity(self.zones.len()); + for zone in self.zones { + let mut options = HashMap::new(); + + for (name, value) in zone.options { + let option = resolve_routing_option(&name, routing_options, "drivers.zones")?; + options.insert(option, resolve_driver_rule_value(&value)?); + } + + zones.push(DriverZoneRule::new( + zone.layer_name, + zone.mask_operator, + zone.mask_threshold, + options, + )); + } + + Ok(DriverRuleSet::new(default, zones)) + } +} + impl FrictionLayerInput { fn into_layer(self, option: u32) -> Result { let multiplier_layer = self.layer_name.or(self.multiplier_layer).ok_or_else(|| { @@ -228,3 +263,23 @@ impl FrictionLayerInput { )) } } + +fn resolve_routing_option(name: &str, routing_options: &[String], context: &str) -> Result { + routing_options + .iter() + .position(|option_name| option_name == name) + .map(|index| index as u32) + .ok_or_else(|| { + crate::error::Error::Undefined(format!("unknown routing option {name:?} in {context}")) + }) +} + +fn resolve_driver_rule_value(value: &DriverRuleValue) -> Result> { + match value { + DriverRuleValue::Multiplier(multiplier) => Ok(Some(*multiplier)), + DriverRuleValue::Keyword(keyword) if keyword == "excluded" => Ok(None), + DriverRuleValue::Keyword(keyword) => Err(crate::error::Error::Undefined(format!( + "unsupported driver rule value {keyword:?}" + ))), + } +} From fb51df198f211737df3c758579654e40188d3051 Mon Sep 17 00:00:00 2001 From: Paul Date: Mon, 1 Jun 2026 16:22:21 -0600 Subject: [PATCH 29/40] inputs in charge of creating pieces --- crates/revrt/src/cost_new/inputs.rs | 32 ++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/crates/revrt/src/cost_new/inputs.rs b/crates/revrt/src/cost_new/inputs.rs index e19ec199..3918b17c 100644 --- a/crates/revrt/src/cost_new/inputs.rs +++ b/crates/revrt/src/cost_new/inputs.rs @@ -6,7 +6,7 @@ use std::collections::HashMap; use crate::cost::{ BarrierLayer, BarrierOperator, CostFunction, CostLayer, DriverRuleSet, DriverZoneRule, - FrictionLayer, + FrictionLayer, TransitionCostTable, }; use crate::error::Result; @@ -183,6 +183,7 @@ impl TryFrom for CostFunction { } let drivers = drivers.into_rule_set(&routing_option_names)?; + let transition_costs = transition_costs.into_table(&routing_option_names)?; Ok(CostFunction::from_input_parts( cost_layers, @@ -248,6 +249,23 @@ impl DriversConfig { } } +impl TransitionCostsConfig { + pub(crate) fn into_table(self, routing_options: &[String]) -> Result { + let mut pairwise = HashMap::new(); + + for rule in self.pairwise { + let from = resolve_transition_option(&rule.from, routing_options)?; + let to = resolve_transition_option(&rule.to, routing_options)?; + pairwise.insert((from, to), rule.cost); + if rule.applies_bidirectionally { + pairwise.insert((to, from), rule.cost); + } + } + + Ok(TransitionCostTable::new(self.default, pairwise)) + } +} + impl FrictionLayerInput { fn into_layer(self, option: u32) -> Result { let multiplier_layer = self.layer_name.or(self.multiplier_layer).ok_or_else(|| { @@ -274,6 +292,18 @@ fn resolve_routing_option(name: &str, routing_options: &[String], context: &str) }) } +fn resolve_transition_option( + option: &TransitionOptionRef, + routing_options: &[String], +) -> Result { + match option { + TransitionOptionRef::Index(index) => Ok(*index), + TransitionOptionRef::Name(name) => { + resolve_routing_option(name, routing_options, "transition_costs") + } + } +} + fn resolve_driver_rule_value(value: &DriverRuleValue) -> Result> { match value { DriverRuleValue::Multiplier(multiplier) => Ok(Some(*multiplier)), From 60eb47ab506b4f74401128a49bbe309bbd1b6edc Mon Sep 17 00:00:00 2001 From: Paul Date: Mon, 1 Jun 2026 16:34:31 -0600 Subject: [PATCH 30/40] Add components --- crates/revrt/src/cost_new/components.rs | 258 ++++++++++++++++++++++++ 1 file changed, 258 insertions(+) create mode 100644 crates/revrt/src/cost_new/components.rs diff --git a/crates/revrt/src/cost_new/components.rs b/crates/revrt/src/cost_new/components.rs new file mode 100644 index 00000000..882a150f --- /dev/null +++ b/crates/revrt/src/cost_new/components.rs @@ -0,0 +1,258 @@ +//! Cost function components + +use core::f32; +use derive_builder::Builder; +use std::collections::HashMap; + +#[derive(Clone, Debug, Default)] +pub(crate) struct TransitionCostTable { + pub(crate) default: f32, + pub(crate) pairwise: HashMap<(u32, u32), f32>, +} + +impl TransitionCostTable { + pub(crate) fn new(default: f32, pairwise: HashMap<(u32, u32), f32>) -> Self { + Self { default, pairwise } + } + + pub(crate) fn cost(&self, from: u32, to: u32) -> f32 { + self.pairwise + .get(&(from, to)) + .copied() + .unwrap_or(self.default) + } +} + +#[derive(Clone, Debug, Default)] +pub(crate) struct DriverRuleSet { + pub(crate) default: Vec>, + pub(crate) zones: Vec, +} + +impl DriverRuleSet { + pub(crate) fn new(default: Vec>, zones: Vec) -> Self { + Self { default, zones } + } + + pub(crate) fn multiplier(&self, option: u32, mut layer_value: F) -> Option + where + F: FnMut(&str) -> Option, + { + let mut multiplier = self + .default + .get(option as usize) + .copied() + .unwrap_or(Some(1.0)); + + for zone in &self.zones { + let Some(value) = layer_value(&zone.layer_name) else { + continue; + }; + + if zone.matches(value) + && let Some(zone_multiplier) = zone.options.get(&option) + { + multiplier = *zone_multiplier; // TODO: This is not quite right. We want replacement. Maybe error if two zones overlap? + } + } + + multiplier + } +} + +#[derive(Clone, Debug)] +pub(crate) struct DriverZoneRule { + pub(crate) layer_name: String, + pub(crate) operator: BarrierOperator, + pub(crate) threshold: f32, + pub(crate) options: HashMap>, +} + +impl DriverZoneRule { + pub(crate) fn new( + layer_name: String, + operator: BarrierOperator, + threshold: f32, + options: HashMap>, + ) -> Self { + Self { + layer_name, + operator, + threshold, + options, + } + } + + fn matches(&self, value: f32) -> bool { + match self.operator { + BarrierOperator::NotEqual => value != self.threshold, + BarrierOperator::GreaterThan => value > self.threshold, + BarrierOperator::GreaterThanOrEqual => value >= self.threshold, + BarrierOperator::LessThan => value < self.threshold, + BarrierOperator::LessThanOrEqual => value <= self.threshold, + BarrierOperator::Equal => value == self.threshold, + } + } +} + +#[derive(Clone, Copy, Debug, serde::Deserialize)] +pub(crate) enum BarrierOperator { + #[serde(rename = "ne")] + NotEqual, + #[serde(rename = "gt")] + GreaterThan, + #[serde(rename = "ge")] + GreaterThanOrEqual, + #[serde(rename = "lt")] + LessThan, + #[serde(rename = "le")] + LessThanOrEqual, + #[serde(rename = "eq")] + Equal, +} + +#[derive(Builder, Clone, Debug, serde::Deserialize)] +/// A cost layer +/// +/// Each cost layer is a raster dataset, i.e. a regular grid, composed by +/// operating on input features. Following the original `revX` structure, +/// the possible compositions are limited to combinations of the relation +/// `weight * layer_name * multiplier_layer`, where the `weight` and the +/// `multiplier_layer` are optional. Each layer can also be marked as invariant, +/// meaning that its value does not get scaled by the distance traveled +/// through the cell. Instead, the value of the layer is added once, right +/// when the path enters the cell. +pub(crate) struct CostLayer { + pub(crate) layer_name: String, + #[builder(setter(strip_option), default)] + pub(crate) multiplier_scalar: Option, + #[builder(setter(strip_option, into), default)] + pub(crate) multiplier_layer: Option, + #[builder(setter(strip_option), default)] + pub(crate) is_invariant: Option, + #[builder(default, setter(skip))] + #[serde(skip)] + pub(crate) option: u32, +} + +impl CostLayer { + pub(crate) fn with_option(mut self, option: u32) -> Self { + self.option = option; + self + } +} + +#[derive(Builder, Clone, Debug, serde::Deserialize)] +/// A friction layer +/// +/// Each friction layer is a raster dataset, i.e. a regular grid, that +/// represents multipliers that should be applied to the cost routing +/// layer. These multipliers affect the output route but will not be +/// reported in the output cost. Each friction layer is defined by a +/// `multiplier_layer` and an optional `multiplier_scalar`. The friction +/// value at each cell is computed as `multiplier_layer * multiplier_scalar`. +/// If the `multiplier_scalar` is not provided, it defaults to 1.0. +/// Friction layers are summed together to produce the final friction +/// layer that is applied to the cost layer. A clamp is applied to the +/// final friction layer to ensure that no values are below -1.0, which +/// would lead to negative routing costs. +pub(crate) struct FrictionLayer { + pub(crate) multiplier_layer: String, + #[builder(setter(strip_option), default)] + pub(crate) multiplier_scalar: Option, + #[serde(skip)] + pub(crate) option: u32, +} + +impl FrictionLayer { + pub(crate) fn new( + multiplier_layer: String, + multiplier_scalar: Option, + option: u32, + ) -> Self { + Self { + multiplier_layer, + multiplier_scalar, + option, + } + } +} + +#[derive(Clone, Debug, serde::Deserialize)] +pub(crate) struct BarrierLayer { + pub(crate) layer_name: String, + pub(crate) barrier_operator: BarrierOperator, + pub(crate) barrier_threshold: f32, + pub(crate) barrier_importance: Option, + #[serde(skip)] + pub(crate) option: u32, +} + +impl BarrierLayer { + pub(crate) fn layer_name(&self) -> &str { + &self.layer_name + } + + pub(crate) fn importance(&self) -> Option { + self.barrier_importance + } + + pub(crate) fn with_option(mut self, option: u32) -> Self { + self.option = option; + self + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn cost_layer_builder_sets_configured_values() { + let layer = CostLayerBuilder::default() + .layer_name("A".to_string()) + .multiplier_scalar(2.0) + .multiplier_layer("B") + .is_invariant(false) + .build() + .unwrap(); + + assert_eq!(layer.layer_name, "A".to_string()); + assert_eq!(layer.multiplier_scalar, Some(2.0)); + assert_eq!(layer.multiplier_layer, Some("B".to_string())); + assert_eq!(layer.is_invariant, Some(false)); + assert_eq!(layer.option, 0); + } + + #[test] + fn cost_layer_builder_applies_defaults() { + let layer = CostLayerBuilder::default() + .layer_name("A".to_string()) + .build() + .unwrap(); + + assert_eq!(layer.layer_name, "A".to_string()); + assert_eq!(layer.multiplier_scalar, None); + assert_eq!(layer.multiplier_layer, None); + assert_eq!(layer.is_invariant, None); + assert_eq!(layer.option, 0); + } + + #[test] + fn driver_rule_set_supports_zone_overrides_and_exclusions() { + let driver_rules = DriverRuleSet::new( + vec![Some(1.0), None], + vec![DriverZoneRule::new( + "zone".to_string(), + BarrierOperator::Equal, + 1.0, + HashMap::from([(0, Some(10.0)), (1, Some(1.0))]), + )], + ); + + assert_eq!(driver_rules.multiplier(0, |_| Some(0.0)), Some(1.0)); + assert_eq!(driver_rules.multiplier(1, |_| Some(0.0)), None); + assert_eq!(driver_rules.multiplier(0, |_| Some(1.0)), Some(10.0)); + assert_eq!(driver_rules.multiplier(1, |_| Some(1.0)), Some(1.0)); + } +} From 17f8bf2cd281a277e18a6b871194768a3e82df26 Mon Sep 17 00:00:00 2001 From: Paul Date: Mon, 1 Jun 2026 16:34:48 -0600 Subject: [PATCH 31/40] Cost function uses new components --- crates/revrt/src/cost.rs | 401 ++++++++++++++++++++++++++------------- 1 file changed, 270 insertions(+), 131 deletions(-) diff --git a/crates/revrt/src/cost.rs b/crates/revrt/src/cost.rs index e075d7a9..9adb83cd 100644 --- a/crates/revrt/src/cost.rs +++ b/crates/revrt/src/cost.rs @@ -1,11 +1,14 @@ //! Cost function use core::f32; -use derive_builder::Builder; use ndarray::{ArrayD, Axis, IxDyn, stack}; use std::convert::TryFrom; use tracing::{debug, trace}; +pub(crate) use crate::cost_new::components::{ + BarrierLayer, BarrierOperator, CostLayer, DriverRuleSet, FrictionLayer, TransitionCostTable, +}; +use crate::cost_new::inputs::CostFunctionInput; use crate::dataset::LazySubset; use crate::error::Result; @@ -16,11 +19,7 @@ type BarrierArray = ndarray::Array>; /// Large friction value to use for invalid costs that can be routed through const HIGH_FRICTION_INVALID_COST: f32 = 1e10; -fn true_option() -> bool { - true -} - -#[derive(Clone, Debug, serde::Deserialize)] +#[derive(Clone, Debug)] /// A cost function definition /// /// `cost_layers`: A collection of cost layers with equal weight. @@ -34,87 +33,34 @@ pub(crate) struct CostFunction { cost_layers: Vec, friction_layers: Option>, barrier_layers: Option>, + pub(crate) routing_options: Vec, + pub(crate) drivers: DriverRuleSet, + pub(crate) transition_costs: TransitionCostTable, /// Option to completely ignore <=0 cost cells - #[serde(default = "true_option")] pub(crate) ignore_invalid_costs: bool, } -#[derive(Clone, Copy, Debug, serde::Deserialize)] -pub(crate) enum BarrierOperator { - #[serde(rename = "ne")] - NotEqual, - #[serde(rename = "gt")] - GreaterThan, - #[serde(rename = "ge")] - GreaterThanOrEqual, - #[serde(rename = "lt")] - LessThan, - #[serde(rename = "le")] - LessThanOrEqual, - #[serde(rename = "eq")] - Equal, -} - -#[derive(Builder, Clone, Debug, serde::Deserialize)] -/// A cost layer -/// -/// Each cost layer is a raster dataset, i.e. a regular grid, composed by -/// operating on input features. Following the original `revX` structure, -/// the possible compositions are limited to combinations of the relation -/// `weight * layer_name * multiplier_layer`, where the `weight` and the -/// `multiplier_layer` are optional. Each layer can also be marked as invariant, -/// meaning that its value does not get scaled by the distance traveled -/// through the cell. Instead, the value of the layer is added once, right -/// when the path enters the cell. -struct CostLayer { - layer_name: String, - #[builder(setter(strip_option), default)] - multiplier_scalar: Option, - #[builder(setter(strip_option, into), default)] - multiplier_layer: Option, - #[builder(setter(strip_option), default)] - is_invariant: Option, -} - -#[derive(Builder, Clone, Debug, serde::Deserialize)] -/// A friction layer -/// -/// Each friction layer is a raster dataset, i.e. a regular grid, that -/// represents multipliers that should be applied to the cost routing -/// layer. These multipliers affect the output route but will not be -/// reported in the output cost. Each friction layer is defined by a -/// `multiplier_layer` and an optional `multiplier_scalar`. The friction -/// value at each cell is computed as `multiplier_layer * multiplier_scalar`. -/// If the `multiplier_scalar` is not provided, it defaults to 1.0. -/// Friction layers are summed together to produce the final friction -/// layer that is applied to the cost layer. A clamp is applied to the -/// final friction layer to ensure that no values are below -1.0, which -/// would lead to negative routing costs. -struct FrictionLayer { - multiplier_layer: String, - #[builder(setter(strip_option), default)] - multiplier_scalar: Option, -} - -#[derive(Clone, Debug, serde::Deserialize)] -pub(crate) struct BarrierLayer { - layer_name: String, - barrier_operator: BarrierOperator, - barrier_threshold: f32, - barrier_importance: Option, -} - -impl BarrierLayer { - pub(crate) fn layer_name(&self) -> &str { - &self.layer_name - } - - pub(crate) fn importance(&self) -> Option { - self.barrier_importance +impl CostFunction { + pub(crate) fn from_input_parts( + cost_layers: Vec, + friction_layers: Vec, + barrier_layers: Vec, + routing_options: Vec, + drivers: DriverRuleSet, + transition_costs: TransitionCostTable, + ignore_invalid_costs: bool, + ) -> Self { + Self { + cost_layers, + friction_layers: (!friction_layers.is_empty()).then_some(friction_layers), + barrier_layers: (!barrier_layers.is_empty()).then_some(barrier_layers), + routing_options, + drivers, + transition_costs, + ignore_invalid_costs, + } } -} -impl CostFunction { /// Create a new cost function from a JSON string (reVX format) /// /// # Arguments @@ -124,30 +70,35 @@ impl CostFunction { /// # Returns /// A `CostFunction` object. /// - /// The JSON pattern used by reVX was the following: + /// Layer definitions must be nested under `routing_options`. /// ```json /// { - /// "cost_layers": [ - /// {"layer_name": "A"}, - /// { - /// "layer_name": "A", - /// "multiplier_scalar": 2, - /// "multiplier_layer": "B" + /// "routing_options": { + /// "default": { + /// "cost_layers": [ + /// {"layer_name": "A"}, + /// { + /// "layer_name": "A", + /// "multiplier_scalar": 2, + /// "multiplier_layer": "B" + /// } + /// ], + /// "barrier_layers": [ + /// { + /// "layer_name": "barrier_mask", + /// "barrier_operator": "eq", + /// "barrier_threshold": 1.0 + /// } + /// ] /// } - /// ], - /// "barrier_layers": [ - /// { - /// "layer_name": "barrier_mask", - /// "barrier_operator": "eq", - /// "barrier_threshold": 1.0 - /// } - /// ] + /// } /// } /// ``` pub(super) fn from_json(json: &str) -> Result { trace!("Parsing cost definition from json: {}", json); - let cost = serde_json::from_str(json).unwrap(); - Ok(cost) + let cost_input: CostFunctionInput = serde_json::from_str(json) + .map_err(|error| crate::error::Error::Undefined(error.to_string()))?; + Self::try_from(cost_input) } /// Return a copy of this cost function with all barrier layers removed. @@ -304,7 +255,7 @@ fn build_single_cost_layer(layer: &CostLayer, features: &mut LazySubset) -> cost = cost * multiplier_value; // trace!( "Cost for chunk ({}, {}) in layer {}: {}", ci, cj, layer_name, cost); } - cost + select_option_for_subset(cost, layer.option, features) } fn build_single_friction_layer(layer: &FrictionLayer, features: &mut LazySubset) -> CostArray { @@ -321,7 +272,7 @@ fn build_single_friction_layer(layer: &FrictionLayer, features: &mut LazySubset< friction *= multiplier_scalar; } - friction + select_option_for_subset(friction, layer.option, features) } pub(crate) fn build_single_barrier_layer( @@ -332,16 +283,51 @@ pub(crate) fn build_single_barrier_layer( let barrier_values = features .get(&layer.layer_name) - .expect("Barrier layer not found in features"); - - barrier_values.mapv(|value| match layer.barrier_operator { - BarrierOperator::NotEqual => value != layer.barrier_threshold, - BarrierOperator::GreaterThan => value > layer.barrier_threshold, - BarrierOperator::GreaterThanOrEqual => value >= layer.barrier_threshold, - BarrierOperator::LessThan => value < layer.barrier_threshold, - BarrierOperator::LessThanOrEqual => value <= layer.barrier_threshold, - BarrierOperator::Equal => value == layer.barrier_threshold, - }) + .expect("Barrier layer not found in features") + .mapv(|value| match layer.barrier_operator { + BarrierOperator::NotEqual => value != layer.barrier_threshold, + BarrierOperator::GreaterThan => value > layer.barrier_threshold, + BarrierOperator::GreaterThanOrEqual => value >= layer.barrier_threshold, + BarrierOperator::LessThan => value < layer.barrier_threshold, + BarrierOperator::LessThanOrEqual => value <= layer.barrier_threshold, + BarrierOperator::Equal => value == layer.barrier_threshold, + }); + + select_option_for_subset(barrier_values, layer.option, features) +} + +fn select_option_for_subset( + values: ndarray::Array>, + option: u32, + features: &LazySubset, +) -> ndarray::Array> +where + T: Clone + Default, +{ + let band_start = features.subset().start()[0]; + let band_end = band_start + features.subset().shape()[0]; + let option = u64::from(option); + + if option < band_start || option >= band_end { + return ndarray::ArrayD::::default(ndarray::IxDyn(values.shape())); + } + + select_option(values, (option - band_start) as u32) +} + +fn select_option( + values: ndarray::Array>, + option: u32, +) -> ndarray::Array> +where + T: Clone + Default, +{ + let option_index = option as usize; + let mut selected = ndarray::ArrayD::::default(ndarray::IxDyn(values.shape())); + selected + .index_axis_mut(Axis(0), option_index) + .assign(&values.index_axis(Axis(0), option_index)); + selected } fn reduce_layers(data: Vec) -> CostArray { @@ -361,16 +347,20 @@ pub(crate) mod sample { pub(crate) fn as_text_v1() -> String { r#" { - "cost_layers": [ - {"layer_name": "A"}, - {"layer_name": "B", "multiplier_scalar": 100}, - {"layer_name": "A", - "multiplier_layer": "B"}, - {"layer_name": "C", "multiplier_scalar": 2, - "multiplier_layer": "A"}, - {"layer_name": "C", "multiplier_scalar": 100, - "is_invariant": true} - ] + "routing_options": { + "default": { + "cost_layers": [ + {"layer_name": "A"}, + {"layer_name": "B", "multiplier_scalar": 100}, + {"layer_name": "A", + "multiplier_layer": "B"}, + {"layer_name": "C", "multiplier_scalar": 2, + "multiplier_layer": "A"}, + {"layer_name": "C", "multiplier_scalar": 100, + "is_invariant": true} + ] + } + } } "# .to_string() @@ -384,7 +374,7 @@ pub(crate) mod sample { #[cfg(test)] mod test_builder { - use super::*; + use crate::cost_new::components::CostLayerBuilder; #[test] fn costlayer() { @@ -400,6 +390,7 @@ mod test_builder { assert_eq!(layer.multiplier_scalar, Some(2.0)); assert_eq!(layer.multiplier_layer, Some("B".to_string())); assert_eq!(layer.is_invariant, Some(false)); + assert_eq!(layer.option, 0); } #[test] @@ -413,6 +404,7 @@ mod test_builder { assert_eq!(layer.multiplier_scalar, None); assert_eq!(layer.multiplier_layer, None); assert_eq!(layer.is_invariant, None); + assert_eq!(layer.option, 0); } } @@ -449,20 +441,25 @@ mod test { assert_eq!(cost.cost_layers.len(), 5); assert_eq!(cost.cost_layers[0].layer_name, "A".to_string()); assert_eq!(cost.cost_layers[0].is_invariant, None); + assert_eq!(cost.cost_layers[0].option, 0); assert_eq!(cost.cost_layers[1].layer_name, "B".to_string()); assert_eq!(cost.cost_layers[1].multiplier_scalar, Some(100.0)); assert_eq!(cost.cost_layers[1].is_invariant, None); + assert_eq!(cost.cost_layers[1].option, 0); assert_eq!(cost.cost_layers[2].layer_name, "A".to_string()); assert_eq!(cost.cost_layers[2].multiplier_layer, Some("B".to_string())); assert_eq!(cost.cost_layers[2].is_invariant, None); + assert_eq!(cost.cost_layers[2].option, 0); assert_eq!(cost.cost_layers[3].layer_name, "C".to_string()); assert_eq!(cost.cost_layers[3].multiplier_layer, Some("A".to_string())); assert_eq!(cost.cost_layers[3].multiplier_scalar, Some(2.0)); assert_eq!(cost.cost_layers[3].is_invariant, None); + assert_eq!(cost.cost_layers[3].option, 0); assert_eq!(cost.cost_layers[4].layer_name, "C".to_string()); assert_eq!(cost.cost_layers[4].multiplier_layer, None); assert_eq!(cost.cost_layers[4].multiplier_scalar, Some(100.0)); assert_eq!(cost.cost_layers[4].is_invariant, Some(true)); + assert_eq!(cost.cost_layers[4].option, 0); } #[test] @@ -484,6 +481,7 @@ mod test { barrier_operator: BarrierOperator::NotEqual, barrier_threshold: 0.0, barrier_importance: None, + option: 0, }; let barrier = build_single_barrier_layer(&layer, &mut features); @@ -501,10 +499,14 @@ mod test { // friction-only (no `layer_name`) should return an empty cost array (zeros) let json = r#" { - "cost_layers": [], - "friction_layers": [ - {"multiplier_layer": "B", "multiplier_scalar": -3.0} - ] + "routing_options": { + "default": { + "cost_layers": [], + "friction_layers": [ + {"multiplier_layer": "B", "multiplier_scalar": -3.0} + ] + } + } } "#; @@ -526,12 +528,16 @@ mod test { // cost layer A with a friction layer defined by B * -3.0 let json = r#" { - "cost_layers": [ - {"layer_name": "A"} - ], - "friction_layers": [ - {"multiplier_layer": "B", "multiplier_scalar": -3.0} - ] + "routing_options": { + "default": { + "cost_layers": [ + {"layer_name": "A"} + ], + "friction_layers": [ + {"multiplier_layer": "B", "multiplier_scalar": -3.0} + ] + } + } } "#; @@ -561,4 +567,137 @@ mod test { assert!(diff < 1e-6, "mismatch {} vs {} (diff={})", r, truth, diff); }); } + + #[test] + fn routing_options_object_builds_ordered_names_and_band_specific_costs() { + let tmp = samples::ZarrTestBuilder::new() + .dimensions(2, 2, 2) + .chunks(2, 2, 2) + .layer(samples::LayerConfig::custom( + "overhead_cost", + |band, _, _| { + if band == 0 { 1.0 } else { 9.0 } + }, + )) + .layer(samples::LayerConfig::custom( + "underground_cost", + |band, _, _| { + if band == 1 { 2.0 } else { 8.0 } + }, + )) + .build() + .expect("Failed to create routing option zarr"); + let store: ReadableListableStorage = Arc::new(FilesystemStore::new(tmp.path()).unwrap()); + let subset = ArraySubset::new_with_start_shape(vec![0, 0, 0], vec![2, 2, 2]).unwrap(); + let mut features = make_lazy_subset_for_tests(store, subset); + let cost_fn = CostFunction::from_json( + r#"{ + "routing_options": { + "overhead": { + "cost_layers": [{"layer_name": "overhead_cost"}] + }, + "underground": { + "cost_layers": [{"layer_name": "underground_cost"}] + } + } + }"#, + ) + .unwrap(); + + let result = cost_fn.compute(&mut features, false); + + assert_eq!(cost_fn.routing_options, ["overhead", "underground"]); + assert_eq!( + result.index_axis(Axis(0), 0).to_owned(), + ArrayD::from_elem(IxDyn(&[2, 2]), 1.0) + ); + assert_eq!( + result.index_axis(Axis(0), 1).to_owned(), + ArrayD::from_elem(IxDyn(&[2, 2]), 2.0) + ); + } + + #[test] + fn routing_options_object_reuses_same_source_layer_across_options() { + let tmp = samples::ZarrTestBuilder::new() + .dimensions(2, 2, 2) + .chunks(2, 2, 2) + .layer(samples::LayerConfig::custom("shared_cost", |band, _, _| { + if band == 0 { 3.0 } else { 4.0 } + })) + .build() + .expect("Failed to create shared routing option zarr"); + let store: ReadableListableStorage = Arc::new(FilesystemStore::new(tmp.path()).unwrap()); + let subset = ArraySubset::new_with_start_shape(vec![0, 0, 0], vec![2, 2, 2]).unwrap(); + let mut features = make_lazy_subset_for_tests(store, subset); + let cost_fn = CostFunction::from_json( + r#"{ + "routing_options": { + "overhead": { + "cost_layers": [{"layer_name": "shared_cost"}] + }, + "underground": { + "cost_layers": [{"layer_name": "shared_cost", "multiplier_scalar": 2.0}] + } + } + }"#, + ) + .unwrap(); + + let result = cost_fn.compute(&mut features, false); + + assert_eq!(cost_fn.routing_options, ["overhead", "underground"]); + assert_eq!( + result.index_axis(Axis(0), 0).to_owned(), + ArrayD::from_elem(IxDyn(&[2, 2]), 3.0) + ); + assert_eq!( + result.index_axis(Axis(0), 1).to_owned(), + ArrayD::from_elem(IxDyn(&[2, 2]), 8.0) + ); + } + + #[test] + fn routing_options_object_supports_sample_barrier_and_friction_syntax() { + let cost_fn = CostFunction::from_json( + r#"{ + "routing_options": { + "overhead": { + "cost_layers": [{"layer_name": "A"}], + "friction_layers": [{"layer_name": "wet_friction", "multiplier_scalar": 1.1}], + "barrier_layers": [{"layer_name": "barrier_mask", "barrier_operator": "eq", "barrier_threshold": 1.0}] + } + } + }"#, + ) + .unwrap(); + + assert_eq!(cost_fn.routing_options, ["overhead"]); + assert_eq!( + cost_fn.friction_layers.as_ref().unwrap()[0].multiplier_layer, + "wet_friction" + ); + assert_eq!( + cost_fn.hard_barrier_layers()[0].barrier_operator as u8, + BarrierOperator::Equal as u8 + ); + assert_eq!(cost_fn.hard_barrier_layers()[0].barrier_threshold, 1.0); + } + + #[test] + fn barrier_layers_require_split_operator_and_threshold_inputs() { + let error = CostFunction::from_json( + r#"{ + "routing_options": { + "overhead": { + "cost_layers": [{"layer_name": "A"}], + "barrier_layers": [{"layer_name": "barrier_mask", "barrier_values": "==1"}] + } + } + }"#, + ) + .unwrap_err(); + + assert!(matches!(error, crate::error::Error::Undefined(_))); + } } From 72f6dc80643dfd6af57fbd74e5079287ea4d0354 Mon Sep 17 00:00:00 2001 From: Paul Date: Mon, 1 Jun 2026 16:41:51 -0600 Subject: [PATCH 32/40] Use new components --- crates/revrt/src/cost_new/inputs.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/revrt/src/cost_new/inputs.rs b/crates/revrt/src/cost_new/inputs.rs index 3918b17c..e7fa909a 100644 --- a/crates/revrt/src/cost_new/inputs.rs +++ b/crates/revrt/src/cost_new/inputs.rs @@ -4,9 +4,10 @@ use serde::de::DeserializeOwned; use serde_json::{Map, Value}; use std::collections::HashMap; -use crate::cost::{ - BarrierLayer, BarrierOperator, CostFunction, CostLayer, DriverRuleSet, DriverZoneRule, - FrictionLayer, TransitionCostTable, +use crate::cost::CostFunction; +use crate::cost_new::components::{ + BarrierLayer, BarrierOperator, CostLayer, DriverRuleSet, DriverZoneRule, FrictionLayer, + TransitionCostTable, }; use crate::error::Result; From 2f71281c6cc82ec1ad6f7b22a2a3234dfccf8ff1 Mon Sep 17 00:00:00 2001 From: Paul Date: Mon, 1 Jun 2026 16:42:05 -0600 Subject: [PATCH 33/40] Pull directly from cost function --- crates/revrt/src/routing/scenario.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/revrt/src/routing/scenario.rs b/crates/revrt/src/routing/scenario.rs index 7a8d2ea3..b23553b0 100644 --- a/crates/revrt/src/routing/scenario.rs +++ b/crates/revrt/src/routing/scenario.rs @@ -59,8 +59,8 @@ impl Scenario { ) -> Result { trace!("Opening scenario with: {:?}", store_path.as_ref()); - let driver_rules = cost_function.driver_rule_set()?; - let transition_costs = cost_function.transition_cost_table()?; + let driver_rules = cost_function.drivers.clone(); + let transition_costs = cost_function.transition_costs.clone(); let features = Features::open(&store_path)?; let dataset = crate::dataset::Dataset::open_with_swap( store_path, @@ -94,8 +94,8 @@ impl Scenario { ) -> Result { trace!("Opening scenario with: {:?}", store_path.as_ref()); - let driver_rules = cost_function.driver_rule_set()?; - let transition_costs = cost_function.transition_cost_table()?; + let driver_rules = cost_function.drivers.clone(); + let transition_costs = cost_function.transition_costs.clone(); let features = Features::open(&store_path)?; let dataset = crate::dataset::Dataset::open(store_path, cost_function, cache_size)?; From 29235d0811bddfbd35f1df026d868238d770dccc Mon Sep 17 00:00:00 2001 From: Paul Date: Mon, 1 Jun 2026 16:45:03 -0600 Subject: [PATCH 34/40] Rename module --- crates/revrt/src/{cost_new => cost}/components.rs | 0 crates/revrt/src/{cost_new => cost}/inputs.rs | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename crates/revrt/src/{cost_new => cost}/components.rs (100%) rename crates/revrt/src/{cost_new => cost}/inputs.rs (99%) diff --git a/crates/revrt/src/cost_new/components.rs b/crates/revrt/src/cost/components.rs similarity index 100% rename from crates/revrt/src/cost_new/components.rs rename to crates/revrt/src/cost/components.rs diff --git a/crates/revrt/src/cost_new/inputs.rs b/crates/revrt/src/cost/inputs.rs similarity index 99% rename from crates/revrt/src/cost_new/inputs.rs rename to crates/revrt/src/cost/inputs.rs index e7fa909a..649d627f 100644 --- a/crates/revrt/src/cost_new/inputs.rs +++ b/crates/revrt/src/cost/inputs.rs @@ -5,7 +5,7 @@ use serde_json::{Map, Value}; use std::collections::HashMap; use crate::cost::CostFunction; -use crate::cost_new::components::{ +use crate::cost::components::{ BarrierLayer, BarrierOperator, CostLayer, DriverRuleSet, DriverZoneRule, FrictionLayer, TransitionCostTable, }; From d3768688e1f08cb18fc50a0c8fd78d19b354f3ac Mon Sep 17 00:00:00 2001 From: Paul Date: Mon, 1 Jun 2026 16:45:09 -0600 Subject: [PATCH 35/40] Move to new module --- crates/revrt/src/{cost.rs => cost/mod.rs} | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) rename crates/revrt/src/{cost.rs => cost/mod.rs} (99%) diff --git a/crates/revrt/src/cost.rs b/crates/revrt/src/cost/mod.rs similarity index 99% rename from crates/revrt/src/cost.rs rename to crates/revrt/src/cost/mod.rs index 9adb83cd..a3a24f20 100644 --- a/crates/revrt/src/cost.rs +++ b/crates/revrt/src/cost/mod.rs @@ -1,14 +1,15 @@ -//! Cost function +pub(crate) mod components; +pub(crate) mod inputs; use core::f32; use ndarray::{ArrayD, Axis, IxDyn, stack}; use std::convert::TryFrom; use tracing::{debug, trace}; -pub(crate) use crate::cost_new::components::{ +pub(crate) use crate::cost::components::{ BarrierLayer, BarrierOperator, CostLayer, DriverRuleSet, FrictionLayer, TransitionCostTable, }; -use crate::cost_new::inputs::CostFunctionInput; +use crate::cost::inputs::CostFunctionInput; use crate::dataset::LazySubset; use crate::error::Result; @@ -374,7 +375,7 @@ pub(crate) mod sample { #[cfg(test)] mod test_builder { - use crate::cost_new::components::CostLayerBuilder; + use crate::cost::components::CostLayerBuilder; #[test] fn costlayer() { From 554a68527f319312f109f1354712147ae381ec8e Mon Sep 17 00:00:00 2001 From: Paul Date: Mon, 1 Jun 2026 18:37:38 -0600 Subject: [PATCH 36/40] Privatize components and methods --- crates/revrt/src/cost/components.rs | 64 +++++++++++++-------------- crates/revrt/src/cost/inputs.rs | 66 ++++++++++++++-------------- crates/revrt/src/cost/mod.rs | 6 +-- crates/revrt/src/dataset/derived.rs | 3 +- crates/revrt/src/dataset/mod.rs | 3 +- crates/revrt/src/routing/scenario.rs | 4 +- 6 files changed, 72 insertions(+), 74 deletions(-) diff --git a/crates/revrt/src/cost/components.rs b/crates/revrt/src/cost/components.rs index 882a150f..18f8b148 100644 --- a/crates/revrt/src/cost/components.rs +++ b/crates/revrt/src/cost/components.rs @@ -6,12 +6,12 @@ use std::collections::HashMap; #[derive(Clone, Debug, Default)] pub(crate) struct TransitionCostTable { - pub(crate) default: f32, - pub(crate) pairwise: HashMap<(u32, u32), f32>, + pub(super) default: f32, + pub(super) pairwise: HashMap<(u32, u32), f32>, } impl TransitionCostTable { - pub(crate) fn new(default: f32, pairwise: HashMap<(u32, u32), f32>) -> Self { + pub(super) fn new(default: f32, pairwise: HashMap<(u32, u32), f32>) -> Self { Self { default, pairwise } } @@ -25,12 +25,12 @@ impl TransitionCostTable { #[derive(Clone, Debug, Default)] pub(crate) struct DriverRuleSet { - pub(crate) default: Vec>, - pub(crate) zones: Vec, + pub(super) default: Vec>, + pub(super) zones: Vec, } impl DriverRuleSet { - pub(crate) fn new(default: Vec>, zones: Vec) -> Self { + pub(super) fn new(default: Vec>, zones: Vec) -> Self { Self { default, zones } } @@ -61,15 +61,15 @@ impl DriverRuleSet { } #[derive(Clone, Debug)] -pub(crate) struct DriverZoneRule { - pub(crate) layer_name: String, - pub(crate) operator: BarrierOperator, - pub(crate) threshold: f32, - pub(crate) options: HashMap>, +pub(super) struct DriverZoneRule { + pub(super) layer_name: String, + pub(super) operator: BarrierOperator, + pub(super) threshold: f32, + pub(super) options: HashMap>, } impl DriverZoneRule { - pub(crate) fn new( + pub(super) fn new( layer_name: String, operator: BarrierOperator, threshold: f32, @@ -96,7 +96,7 @@ impl DriverZoneRule { } #[derive(Clone, Copy, Debug, serde::Deserialize)] -pub(crate) enum BarrierOperator { +pub(super) enum BarrierOperator { #[serde(rename = "ne")] NotEqual, #[serde(rename = "gt")] @@ -122,21 +122,21 @@ pub(crate) enum BarrierOperator { /// meaning that its value does not get scaled by the distance traveled /// through the cell. Instead, the value of the layer is added once, right /// when the path enters the cell. -pub(crate) struct CostLayer { +pub(super) struct CostLayer { pub(crate) layer_name: String, #[builder(setter(strip_option), default)] - pub(crate) multiplier_scalar: Option, + pub(super) multiplier_scalar: Option, #[builder(setter(strip_option, into), default)] - pub(crate) multiplier_layer: Option, + pub(super) multiplier_layer: Option, #[builder(setter(strip_option), default)] - pub(crate) is_invariant: Option, + pub(super) is_invariant: Option, #[builder(default, setter(skip))] #[serde(skip)] - pub(crate) option: u32, + pub(super) option: u32, } impl CostLayer { - pub(crate) fn with_option(mut self, option: u32) -> Self { + pub(super) fn with_option(mut self, option: u32) -> Self { self.option = option; self } @@ -156,16 +156,16 @@ impl CostLayer { /// layer that is applied to the cost layer. A clamp is applied to the /// final friction layer to ensure that no values are below -1.0, which /// would lead to negative routing costs. -pub(crate) struct FrictionLayer { - pub(crate) multiplier_layer: String, +pub(super) struct FrictionLayer { + pub(super) multiplier_layer: String, #[builder(setter(strip_option), default)] - pub(crate) multiplier_scalar: Option, + pub(super) multiplier_scalar: Option, #[serde(skip)] - pub(crate) option: u32, + pub(super) option: u32, } impl FrictionLayer { - pub(crate) fn new( + pub(super) fn new( multiplier_layer: String, multiplier_scalar: Option, option: u32, @@ -181,23 +181,19 @@ impl FrictionLayer { #[derive(Clone, Debug, serde::Deserialize)] pub(crate) struct BarrierLayer { pub(crate) layer_name: String, - pub(crate) barrier_operator: BarrierOperator, - pub(crate) barrier_threshold: f32, - pub(crate) barrier_importance: Option, + pub(super) barrier_operator: BarrierOperator, + pub(super) barrier_threshold: f32, + pub(super) barrier_importance: Option, #[serde(skip)] - pub(crate) option: u32, + pub(super) option: u32, } impl BarrierLayer { - pub(crate) fn layer_name(&self) -> &str { - &self.layer_name - } - - pub(crate) fn importance(&self) -> Option { + pub(super) fn importance(&self) -> Option { self.barrier_importance } - pub(crate) fn with_option(mut self, option: u32) -> Self { + pub(super) fn with_option(mut self, option: u32) -> Self { self.option = option; self } diff --git a/crates/revrt/src/cost/inputs.rs b/crates/revrt/src/cost/inputs.rs index 649d627f..ea74e4fc 100644 --- a/crates/revrt/src/cost/inputs.rs +++ b/crates/revrt/src/cost/inputs.rs @@ -16,7 +16,7 @@ fn true_option() -> bool { } #[derive(Clone, Debug, serde::Deserialize)] -pub(crate) struct CostFunctionInput { +pub(super) struct CostFunctionInput { #[serde(default)] routing_options: RoutingOptionsInput, #[serde(default)] @@ -29,21 +29,21 @@ pub(crate) struct CostFunctionInput { #[derive(Clone, Debug, Default, serde::Deserialize)] #[serde(untagged)] -pub(crate) enum RoutingOptionsInput { +pub(super) enum RoutingOptionsInput { Definitions(Map), #[default] Missing, } #[derive(Clone, Debug)] -pub(crate) struct RoutingOptionEntry { - pub(crate) name: String, - pub(crate) index: u32, - pub(crate) definition: TDefinition, +pub(super) struct RoutingOptionEntry { + pub(super) name: String, + pub(super) index: u32, + pub(super) definition: TDefinition, } #[derive(Clone, Debug, Default, serde::Deserialize)] -pub(crate) struct RoutingOptionDefinition { +pub(super) struct RoutingOptionDefinition { #[serde(default)] cost_layers: Vec, #[serde(default)] @@ -53,10 +53,10 @@ pub(crate) struct RoutingOptionDefinition { } #[derive(Clone, Debug, Default)] -pub(crate) struct RoutingOptionLayerSet { - pub(crate) cost_layers: Vec, - pub(crate) friction_layers: Vec, - pub(crate) barrier_layers: Vec, +pub(super) struct RoutingOptionLayerSet { + pub(super) cost_layers: Vec, + pub(super) friction_layers: Vec, + pub(super) barrier_layers: Vec, } #[derive(Clone, Debug, serde::Deserialize)] @@ -70,55 +70,55 @@ struct FrictionLayerInput { } #[derive(Clone, Debug, Default, serde::Deserialize)] -pub(crate) struct TransitionCostsConfig { +pub(super) struct TransitionCostsConfig { #[serde(default)] - pub(crate) default: f32, + pub(super) default: f32, #[serde(default)] - pub(crate) pairwise: Vec, + pub(super) pairwise: Vec, } #[derive(Clone, Debug, Default, serde::Deserialize)] -pub(crate) struct DriversConfig { +pub(super) struct DriversConfig { #[serde(default)] - pub(crate) default: HashMap, + pub(super) default: HashMap, #[serde(default)] - pub(crate) zones: Vec, + pub(super) zones: Vec, } #[derive(Clone, Debug, serde::Deserialize)] -pub(crate) struct DriverZoneConfig { - pub(crate) layer_name: String, - pub(crate) mask_operator: BarrierOperator, - pub(crate) mask_threshold: f32, +pub(super) struct DriverZoneConfig { + pub(super) layer_name: String, + pub(super) mask_operator: BarrierOperator, + pub(super) mask_threshold: f32, #[serde(flatten)] - pub(crate) options: HashMap, + pub(super) options: HashMap, } #[derive(Clone, Debug, serde::Deserialize)] #[serde(untagged)] -pub(crate) enum DriverRuleValue { +pub(super) enum DriverRuleValue { Keyword(String), Multiplier(f32), } #[derive(Clone, Debug, serde::Deserialize)] -pub(crate) struct TransitionCostRule { - pub(crate) from: TransitionOptionRef, - pub(crate) to: TransitionOptionRef, - pub(crate) cost: f32, +pub(super) struct TransitionCostRule { + pub(super) from: TransitionOptionRef, + pub(super) to: TransitionOptionRef, + pub(super) cost: f32, #[serde(default)] - pub(crate) applies_bidirectionally: bool, + pub(super) applies_bidirectionally: bool, } #[derive(Clone, Debug, serde::Deserialize)] #[serde(untagged)] -pub(crate) enum TransitionOptionRef { +pub(super) enum TransitionOptionRef { Index(u32), Name(String), } impl RoutingOptionsInput { - pub(crate) fn into_entries(self) -> Result>> + pub(super) fn into_entries(self) -> Result>> where TDefinition: DeserializeOwned, { @@ -199,7 +199,7 @@ impl TryFrom for CostFunction { } impl RoutingOptionDefinition { - pub(crate) fn into_layers(self, option: u32) -> Result { + pub(super) fn into_layers(self, option: u32) -> Result { Ok(RoutingOptionLayerSet { cost_layers: self .cost_layers @@ -221,7 +221,7 @@ impl RoutingOptionDefinition { } impl DriversConfig { - pub(crate) fn into_rule_set(self, routing_options: &[String]) -> Result { + pub(super) fn into_rule_set(self, routing_options: &[String]) -> Result { let mut default = vec![Some(1.0); routing_options.len()]; for (name, value) in self.default { @@ -251,7 +251,7 @@ impl DriversConfig { } impl TransitionCostsConfig { - pub(crate) fn into_table(self, routing_options: &[String]) -> Result { + pub(super) fn into_table(self, routing_options: &[String]) -> Result { let mut pairwise = HashMap::new(); for rule in self.pairwise { diff --git a/crates/revrt/src/cost/mod.rs b/crates/revrt/src/cost/mod.rs index a3a24f20..57f58e59 100644 --- a/crates/revrt/src/cost/mod.rs +++ b/crates/revrt/src/cost/mod.rs @@ -1,12 +1,12 @@ pub(crate) mod components; -pub(crate) mod inputs; +mod inputs; use core::f32; use ndarray::{ArrayD, Axis, IxDyn, stack}; use std::convert::TryFrom; use tracing::{debug, trace}; -pub(crate) use crate::cost::components::{ +use crate::cost::components::{ BarrierLayer, BarrierOperator, CostLayer, DriverRuleSet, FrictionLayer, TransitionCostTable, }; use crate::cost::inputs::CostFunctionInput; @@ -42,7 +42,7 @@ pub(crate) struct CostFunction { } impl CostFunction { - pub(crate) fn from_input_parts( + fn from_input_parts( cost_layers: Vec, friction_layers: Vec, barrier_layers: Vec, diff --git a/crates/revrt/src/dataset/derived.rs b/crates/revrt/src/dataset/derived.rs index 65d8b75c..d1926c34 100644 --- a/crates/revrt/src/dataset/derived.rs +++ b/crates/revrt/src/dataset/derived.rs @@ -15,7 +15,8 @@ use super::LazySubset; use super::reader::DerivedDataMaterializer; use super::swap::SourceLayout; use super::swap::cumulative_soft_barrier_mask_name; -use crate::cost::{BarrierLayer, CostFunction}; +use crate::cost::CostFunction; +use crate::cost::components::BarrierLayer; /// Writes derived chunk-level arrays into the swap dataset. /// diff --git a/crates/revrt/src/dataset/mod.rs b/crates/revrt/src/dataset/mod.rs index ea107e39..d1e90339 100644 --- a/crates/revrt/src/dataset/mod.rs +++ b/crates/revrt/src/dataset/mod.rs @@ -29,7 +29,8 @@ use zarrs::array::{Array, DataType, ElementOwned}; use zarrs::storage::ReadableListableStorage; use crate::ArrayIndex; -use crate::cost::{BarrierLayer, CostFunction}; +use crate::cost::CostFunction; +use crate::cost::components::BarrierLayer; use crate::error::Result; use derived::DerivedDataWriter; pub(crate) use lazy_subset::LazySubset; diff --git a/crates/revrt/src/routing/scenario.rs b/crates/revrt/src/routing/scenario.rs index b23553b0..87ee9578 100644 --- a/crates/revrt/src/routing/scenario.rs +++ b/crates/revrt/src/routing/scenario.rs @@ -18,7 +18,7 @@ use std::path::PathBuf; use tracing::trace; use super::cost_as_u64; -use crate::cost::{DriverRuleSet, TransitionCostTable}; +use crate::cost::components::{DriverRuleSet, TransitionCostTable}; use crate::routing::features::Features; use crate::{ArrayIndex, Result}; @@ -217,7 +217,7 @@ impl Scenario { .take(dropped_soft_groups) { for layer in layers { - dropped_barrier_layers.push(layer.layer_name().to_string()); + dropped_barrier_layers.push(layer.layer_name.clone()); } } From b04376596c1950da2d9c0a36bfe7afa15d9f4ff8 Mon Sep 17 00:00:00 2001 From: Paul Date: Mon, 1 Jun 2026 20:06:50 -0600 Subject: [PATCH 37/40] Minimal wire up on python side --- revrt/routing/base.py | 14 +- revrt/routing/cli/build_costs.py | 14 +- revrt/routing/cli/utilities.py | 10 +- .../test_rust_bindings_integration.py | 116 ++++++++----- .../cli/test_routing_cli_build_costs.py | 79 ++++++--- .../python/unit/routing/test_routing_base.py | 4 +- tests/python/unit/test_rust_bindings.py | 162 +++++++++++------- tests/python/unit/test_skimage_validate.py | 10 +- tests/rust/integration_tests.rs | 14 +- 9 files changed, 276 insertions(+), 147 deletions(-) diff --git a/revrt/routing/base.py b/revrt/routing/base.py index 5b3b8217..517c9f67 100644 --- a/revrt/routing/base.py +++ b/revrt/routing/base.py @@ -112,10 +112,18 @@ def cost_function_json(self): """str: JSON string describing configured cost layers""" return json.dumps( { - "cost_layers": list(self._cost_layers_for_rust()), - "friction_layers": list(self._friction_layers_for_rust()), - "barrier_layers": list(self._barrier_layers_for_rust()), "ignore_invalid_costs": self.ignore_invalid_costs, + "routing_options": { + "default": { + "cost_layers": list(self._cost_layers_for_rust()), + "friction_layers": list( + self._friction_layers_for_rust() + ), + "barrier_layers": list( + self._barrier_layers_for_rust() + ), + } + }, } ) diff --git a/revrt/routing/cli/build_costs.py b/revrt/routing/cli/build_costs.py index f4fbf1b6..45068367 100644 --- a/revrt/routing/cli/build_costs.py +++ b/revrt/routing/cli/build_costs.py @@ -15,7 +15,7 @@ def build_final_routing_layers( - lcp_config_fp, output_dir, polarity=None, voltage=None + lcp_config_fp, output_dir, routing_option, polarity=None, voltage=None ): """Build out the final routing layers based on an LCP config file @@ -36,6 +36,11 @@ def build_final_routing_layers( created. output_dir : path-like Path to directory where to store the outputs. + routing_option : str + Routing option to use when building the routing layer. This + input is used to determine which cost and friction layers to use + when building the routing layer. The routing option must be one + of the options specified in the config file. polarity : str, optional Polarity to use when building the routing layer. This input is required if any cost or friction layers that have @@ -63,14 +68,15 @@ def build_final_routing_layers( config=config.get("transmission_config") ) + route_layer_config = config["routing_options"][routing_option] route_cl = update_multipliers( - config["cost_layers"], + route_layer_config["cost_layers"], polarity, voltage, transmission_config, ) route_fl = update_multipliers( - config.get("friction_layers") or [], + route_layer_config.get("friction_layers") or [], polarity, voltage, transmission_config, @@ -80,7 +86,7 @@ def build_final_routing_layers( cost_fpath=config["cost_fpath"], cost_layers=route_cl, friction_layers=route_fl, - barrier_layers=config.get("barrier_layers"), + barrier_layers=route_layer_config.get("barrier_layers"), cost_multiplier_layer=config.get("cost_multiplier_layer"), cost_multiplier_scalar=config.get("cost_multiplier_scalar", 1), ignore_invalid_costs=config.get("ignore_invalid_costs", False), diff --git a/revrt/routing/cli/utilities.py b/revrt/routing/cli/utilities.py index 57563ece..634d04da 100644 --- a/revrt/routing/cli/utilities.py +++ b/revrt/routing/cli/utilities.py @@ -156,9 +156,13 @@ def _route_layer_hash(cost_layers, friction_layers, barrier_layers): """Compute short hash for layer definitions""" payload = json.dumps( { - "cost_layers": cost_layers, - "friction_layers": friction_layers, - "barrier_layers": barrier_layers, + "routing_options": { + "default": { + "cost_layers": cost_layers, + "friction_layers": friction_layers, + "barrier_layers": barrier_layers, + } + } }, sort_keys=True, separators=(",", ":"), diff --git a/tests/python/integration/test_rust_bindings_integration.py b/tests/python/integration/test_rust_bindings_integration.py index 8817cfc1..621e4134 100644 --- a/tests/python/integration/test_rust_bindings_integration.py +++ b/tests/python/integration/test_rust_bindings_integration.py @@ -100,7 +100,9 @@ def test_find_paths_basic_single_route_layered_file(tmp_path): overwrite=True, ) cost_definition = { - "cost_layers": [{"layer_name": "test_costs"}], + "routing_options": { + "default": {"cost_layers": [{"layer_name": "test_costs"}]} + }, "ignore_invalid_costs": True, } results = find_paths( @@ -142,14 +144,18 @@ def test_find_paths_respects_hard_barrier_layered_file(tmp_path): ) cost_definition = { - "cost_layers": [{"layer_name": "test_costs"}], - "barrier_layers": [ - { - "layer_name": "test_barrier", - "barrier_operator": "eq", - "barrier_threshold": 1, + "routing_options": { + "default": { + "cost_layers": [{"layer_name": "test_costs"}], + "barrier_layers": [ + { + "layer_name": "test_barrier", + "barrier_operator": "eq", + "barrier_threshold": 1, + } + ], } - ], + }, "ignore_invalid_costs": False, } results = find_paths( @@ -183,14 +189,18 @@ def test_find_paths_respects_not_equal_barrier_layered_file(tmp_path): ) cost_definition = { - "cost_layers": [{"layer_name": "test_costs"}], - "barrier_layers": [ - { - "layer_name": "test_barrier", - "barrier_operator": "ne", - "barrier_threshold": 0, + "routing_options": { + "default": { + "cost_layers": [{"layer_name": "test_costs"}], + "barrier_layers": [ + { + "layer_name": "test_barrier", + "barrier_operator": "ne", + "barrier_threshold": 0, + } + ], } - ], + }, "ignore_invalid_costs": False, } results = find_paths( @@ -260,7 +270,11 @@ def test_route_finder_basic_single_route_layered_file(tmp_path, algorithm): "test_costs", overwrite=True, ) - cost_definition = {"cost_layers": [{"layer_name": "test_costs"}]} + cost_definition = { + "routing_options": { + "default": {"cost_layers": [{"layer_name": "test_costs"}]} + } + } routing_results = RouteFinder( zarr_fp=layered_fp, cost_function=json.dumps(cost_definition), @@ -330,20 +344,24 @@ def test_route_finder_retries_soft_barriers_layered_file(tmp_path, algorithm): ) cost_definition = { - "cost_layers": [{"layer_name": "test_costs"}], - "barrier_layers": [ - { - "layer_name": "hard_barrier", - "barrier_operator": "eq", - "barrier_threshold": 1, - }, - { - "layer_name": "soft_barrier", - "barrier_operator": "eq", - "barrier_threshold": 1, - "barrier_importance": 1, - }, - ], + "routing_options": { + "default": { + "cost_layers": [{"layer_name": "test_costs"}], + "barrier_layers": [ + { + "layer_name": "hard_barrier", + "barrier_operator": "eq", + "barrier_threshold": 1, + }, + { + "layer_name": "soft_barrier", + "barrier_operator": "eq", + "barrier_threshold": 1, + "barrier_importance": 1, + }, + ], + } + }, "ignore_invalid_costs": False, } results = list( @@ -422,21 +440,25 @@ def test_route_finder_drops_multiple_soft_barrier_groups_layered_file( ) cost_definition = { - "cost_layers": [{"layer_name": "test_costs"}], - "barrier_layers": [ - { - "layer_name": "soft_barrier_low", - "barrier_operator": "eq", - "barrier_threshold": 1, - "barrier_importance": 1, - }, - { - "layer_name": "soft_barrier_high", - "barrier_operator": "eq", - "barrier_threshold": 1, - "barrier_importance": 2, - }, - ], + "routing_options": { + "default": { + "cost_layers": [{"layer_name": "test_costs"}], + "barrier_layers": [ + { + "layer_name": "soft_barrier_low", + "barrier_operator": "eq", + "barrier_threshold": 1, + "barrier_importance": 1, + }, + { + "layer_name": "soft_barrier_high", + "barrier_operator": "eq", + "barrier_threshold": 1, + "barrier_importance": 2, + }, + ], + } + }, "ignore_invalid_costs": False, } results = list( @@ -519,7 +541,9 @@ def test_route_finder_writes_routing_layer_to_expected_path_layered_file( ) cost_definition = { - "cost_layers": [{"layer_name": "test_costs"}], + "routing_options": { + "default": {"cost_layers": [{"layer_name": "test_costs"}]} + }, "ignore_invalid_costs": True, } routing_layer_out_fp = tmp_path / "routing_layer.zarr" diff --git a/tests/python/unit/routing/cli/test_routing_cli_build_costs.py b/tests/python/unit/routing/cli/test_routing_cli_build_costs.py index 61c72215..7020c708 100644 --- a/tests/python/unit/routing/cli/test_routing_cli_build_costs.py +++ b/tests/python/unit/routing/cli/test_routing_cli_build_costs.py @@ -112,10 +112,14 @@ def test_build_final_routing_layers_command_writes_expected_layers( config = { "cost_fpath": str(sample_layered_data), - "cost_layers": [ - {"layer_name": "layer_1", "multiplier_scalar": 1.5}, - {"layer_name": "layer_2", "multiplier_scalar": 0.5}, - ], + "routing_options": { + "default": { + "cost_layers": [ + {"layer_name": "layer_1", "multiplier_scalar": 1.5}, + {"layer_name": "layer_2", "multiplier_scalar": 0.5}, + ], + } + }, "cost_multiplier_scalar": 2.0, "ignore_invalid_costs": True, } @@ -127,6 +131,7 @@ def test_build_final_routing_layers_command_writes_expected_layers( outputs = build_final_routing_layers_command.runner( lcp_config_fp=config_fp, output_dir=output_dir, + routing_option="default", polarity=None, voltage=None, ) @@ -164,12 +169,16 @@ def test_build_final_routing_layers_command_applies_explicit_barriers( config = { "cost_fpath": str(sample_layered_data), - "cost_layers": [ - {"layer_name": "layer_2"}, - ], - "barrier_layers": [ - {"layer_name": "layer_1", "barrier_values": "==0"}, - ], + "routing_options": { + "default": { + "cost_layers": [ + {"layer_name": "layer_2"}, + ], + "barrier_layers": [ + {"layer_name": "layer_1", "barrier_values": "==0"}, + ], + } + }, "ignore_invalid_costs": False, } @@ -180,6 +189,7 @@ def test_build_final_routing_layers_command_applies_explicit_barriers( outputs = build_final_routing_layers_command.runner( lcp_config_fp=config_fp, output_dir=output_dir, + routing_option="default", polarity=None, voltage=None, ) @@ -216,10 +226,14 @@ def test_build_final_routing_layers_parses_transmission_config_path( config = { "cost_fpath": str(sample_layered_data), - "cost_layers": [ - {"layer_name": "layer_1", "apply_row_mult": True}, - {"layer_name": "layer_2", "apply_polarity_mult": True}, - ], + "routing_options": { + "default": { + "cost_layers": [ + {"layer_name": "layer_1", "apply_row_mult": True}, + {"layer_name": "layer_2", "apply_polarity_mult": True}, + ] + } + }, "transmission_config": str(transmission_config_fp), "ignore_invalid_costs": True, } @@ -231,6 +245,7 @@ def test_build_final_routing_layers_parses_transmission_config_path( outputs = build_final_routing_layers( lcp_config_fp=config_fp, output_dir=output_dir, + routing_option="default", polarity="ac", voltage=138, ) @@ -264,7 +279,9 @@ def test_build_final_routing_layers_writes_to_supplied_output_directory( config = { "cost_fpath": str(sample_layered_data), - "cost_layers": [{"layer_name": "layer_1"}], + "routing_options": { + "default": {"cost_layers": [{"layer_name": "layer_1"}]} + }, "ignore_invalid_costs": True, } @@ -276,6 +293,7 @@ def test_build_final_routing_layers_writes_to_supplied_output_directory( outputs = build_final_routing_layers( lcp_config_fp=config_fp, output_dir=output_dir, + routing_option="default", polarity=None, voltage=None, ) @@ -305,10 +323,14 @@ def test_cli_build_final_routing_layers_command( lcp_config = { "cost_fpath": str(sample_layered_data), - "cost_layers": [ - {"layer_name": "layer_1", "multiplier_scalar": 1.5}, - {"layer_name": "layer_2", "multiplier_scalar": 0.5}, - ], + "routing_options": { + "default": { + "cost_layers": [ + {"layer_name": "layer_1", "multiplier_scalar": 1.5}, + {"layer_name": "layer_2", "multiplier_scalar": 0.5}, + ] + } + }, "cost_multiplier_scalar": 2.0, "ignore_invalid_costs": True, } @@ -316,7 +338,10 @@ def test_cli_build_final_routing_layers_command( lcp_config_fp = tmp_path / "cli_lcp_config.json" lcp_config_fp.write_text(json.dumps(lcp_config)) - cli_config = {"lcp_config_fp": str(lcp_config_fp)} + cli_config = { + "lcp_config_fp": str(lcp_config_fp), + "routing_option": "default", + } cli_config_fp = tmp_path / "cli_command_config.json" cli_config_fp.write_text(json.dumps(cli_config)) @@ -361,14 +386,19 @@ def test_cli_build_route_costs_strips_required_path_whitespace( lcp_config = { "cost_fpath": str(sample_layered_data), - "cost_layers": [{"layer_name": "layer_1"}], + "routing_options": { + "default": {"cost_layers": [{"layer_name": "layer_1"}]} + }, "ignore_invalid_costs": True, } lcp_config_fp = tmp_path / "cli_trimmed_lcp_config.json" lcp_config_fp.write_text(json.dumps(lcp_config)) - cli_config = {"lcp_config_fp": f" {lcp_config_fp} "} + cli_config = { + "lcp_config_fp": f" {lcp_config_fp} ", + "routing_option": "default", + } cli_config_fp = tmp_path / "cli_trimmed_command_config.json" cli_config_fp.write_text(json.dumps(cli_config)) @@ -393,7 +423,9 @@ def test_cli_build_final_routing_layers_honors_output_directory( lcp_config = { "cost_fpath": str(sample_layered_data), - "cost_layers": [{"layer_name": "layer_1"}], + "routing_options": { + "default": {"cost_layers": [{"layer_name": "layer_1"}]} + }, "ignore_invalid_costs": True, } @@ -404,6 +436,7 @@ def test_cli_build_final_routing_layers_honors_output_directory( cli_config = { "lcp_config_fp": str(lcp_config_fp), "output_directory": str(output_dir), + "routing_option": "default", } cli_config_fp = tmp_path / "cli_custom_output_command_config.json" diff --git a/tests/python/unit/routing/test_routing_base.py b/tests/python/unit/routing/test_routing_base.py index bc346905..411d2fd9 100644 --- a/tests/python/unit/routing/test_routing_base.py +++ b/tests/python/unit/routing/test_routing_base.py @@ -2072,7 +2072,7 @@ def test_barrier_layers_are_normalized_for_rust(sample_layered_data): ) cost_function = json.loads(scenario.cost_function_json) - assert cost_function["barrier_layers"] == [ + assert cost_function["routing_options"]["default"]["barrier_layers"] == [ { "layer_name": "layer_4", "barrier_operator": "eq", @@ -2104,7 +2104,7 @@ def test_barrier_layers_normalize_not_equal_for_rust(sample_layered_data): ) cost_function = json.loads(scenario.cost_function_json) - assert cost_function["barrier_layers"] == [ + assert cost_function["routing_options"]["default"]["barrier_layers"] == [ { "layer_name": "layer_4", "barrier_operator": "ne", diff --git a/tests/python/unit/test_rust_bindings.py b/tests/python/unit/test_rust_bindings.py index 030ffa29..fffa952d 100644 --- a/tests/python/unit/test_rust_bindings.py +++ b/tests/python/unit/test_rust_bindings.py @@ -41,7 +41,9 @@ def test_find_paths_basic_single_route(tmp_path): ) cost_definition = { - "cost_layers": [{"layer_name": "test_costs"}], + "routing_options": { + "default": {"cost_layers": [{"layer_name": "test_costs"}]} + }, "ignore_invalid_costs": True, } results = find_paths( @@ -96,14 +98,18 @@ def test_find_paths_respects_explicit_barrier_layers(tmp_path): ) cost_definition = { - "cost_layers": [{"layer_name": "test_costs"}], - "barrier_layers": [ - { - "layer_name": "test_barrier", - "barrier_operator": "eq", - "barrier_threshold": 1, + "routing_options": { + "default": { + "cost_layers": [{"layer_name": "test_costs"}], + "barrier_layers": [ + { + "layer_name": "test_barrier", + "barrier_operator": "eq", + "barrier_threshold": 1, + } + ], } - ], + }, "ignore_invalid_costs": False, } results = find_paths( @@ -150,14 +156,18 @@ def test_find_paths_respects_not_equal_barrier_layers(tmp_path): ) cost_definition = { - "cost_layers": [{"layer_name": "test_costs"}], - "barrier_layers": [ - { - "layer_name": "test_barrier", - "barrier_operator": "ne", - "barrier_threshold": 1, + "routing_options": { + "default": { + "cost_layers": [{"layer_name": "test_costs"}], + "barrier_layers": [ + { + "layer_name": "test_barrier", + "barrier_operator": "ne", + "barrier_threshold": 1, + } + ], } - ], + }, "ignore_invalid_costs": False, } results = find_paths( @@ -208,7 +218,11 @@ def test_route_finder_basic_single_route(tmp_path, algorithm): test_cost_fp, mode="w", zarr_format=3, consolidated=False ) - cost_definition = {"cost_layers": [{"layer_name": "test_costs"}]} + cost_definition = { + "routing_options": { + "default": {"cost_layers": [{"layer_name": "test_costs"}]} + } + } routing_results = RouteFinder( zarr_fp=test_cost_fp, cost_function=json.dumps(cost_definition), @@ -279,7 +293,9 @@ def test_route_finder_writes_routing_layer_to_expected_path( routing_layer_out_fp = tmp_path / "routing_layer.zarr" cost_definition = { - "cost_layers": [{"layer_name": "test_costs"}], + "routing_options": { + "default": {"cost_layers": [{"layer_name": "test_costs"}]} + }, "ignore_invalid_costs": True, } routing_results = RouteFinder( @@ -368,7 +384,9 @@ def test_find_paths_supports_explicit_algorithm(tmp_path, algorithm): ) cost_definition = { - "cost_layers": [{"layer_name": "test_costs"}], + "routing_options": { + "default": {"cost_layers": [{"layer_name": "test_costs"}]} + }, "ignore_invalid_costs": True, } results = find_paths( @@ -427,7 +445,11 @@ def test_route_finder_supports_explicit_algorithm(tmp_path, algorithm): test_cost_fp, mode="w", zarr_format=3, consolidated=False ) - cost_definition = {"cost_layers": [{"layer_name": "test_costs"}]} + cost_definition = { + "routing_options": { + "default": {"cost_layers": [{"layer_name": "test_costs"}]} + } + } results = list( RouteFinder( zarr_fp=test_cost_fp, @@ -501,20 +523,24 @@ def test_route_finder_tracks_dropped_barriers_per_start_point(tmp_path): ) cost_definition = { - "cost_layers": [{"layer_name": "test_costs"}], - "barrier_layers": [ - { - "layer_name": "hard_barrier", - "barrier_operator": "eq", - "barrier_threshold": 1.0, - }, - { - "layer_name": "soft_barrier", - "barrier_operator": "eq", - "barrier_threshold": 1.0, - "barrier_importance": 1, - }, - ], + "routing_options": { + "default": { + "cost_layers": [{"layer_name": "test_costs"}], + "barrier_layers": [ + { + "layer_name": "hard_barrier", + "barrier_operator": "eq", + "barrier_threshold": 1.0, + }, + { + "layer_name": "soft_barrier", + "barrier_operator": "eq", + "barrier_threshold": 1.0, + "barrier_importance": 1, + }, + ], + } + }, "ignore_invalid_costs": False, } results = list( @@ -586,15 +612,19 @@ def test_route_finder_retries_not_equal_soft_barriers(tmp_path): ) cost_definition = { - "cost_layers": [{"layer_name": "test_costs"}], - "barrier_layers": [ - { - "layer_name": "soft_barrier", - "barrier_operator": "ne", - "barrier_threshold": 1, - "barrier_importance": 1, - }, - ], + "routing_options": { + "default": { + "cost_layers": [{"layer_name": "test_costs"}], + "barrier_layers": [ + { + "layer_name": "soft_barrier", + "barrier_operator": "ne", + "barrier_threshold": 1, + "barrier_importance": 1, + }, + ], + } + }, "ignore_invalid_costs": False, } results = list( @@ -681,21 +711,25 @@ def test_route_finder_drops_soft_barriers_by_importance(tmp_path, algorithm): ) cost_definition = { - "cost_layers": [{"layer_name": "test_costs"}], - "barrier_layers": [ - { - "layer_name": "soft_barrier_low", - "barrier_operator": "eq", - "barrier_threshold": 1.0, - "barrier_importance": 1, - }, - { - "layer_name": "soft_barrier_high", - "barrier_operator": "eq", - "barrier_threshold": 1.0, - "barrier_importance": 2, - }, - ], + "routing_options": { + "default": { + "cost_layers": [{"layer_name": "test_costs"}], + "barrier_layers": [ + { + "layer_name": "soft_barrier_low", + "barrier_operator": "eq", + "barrier_threshold": 1.0, + "barrier_importance": 1, + }, + { + "layer_name": "soft_barrier_high", + "barrier_operator": "eq", + "barrier_threshold": 1.0, + "barrier_importance": 2, + }, + ], + } + }, "ignore_invalid_costs": False, } results = list( @@ -742,7 +776,11 @@ def test_find_paths_supports_a_star_alias(tmp_path): results = find_paths( zarr_fp=test_cost_fp, cost_function=json.dumps( - {"cost_layers": [{"layer_name": "test_costs"}]} + { + "routing_options": { + "default": {"cost_layers": [{"layer_name": "test_costs"}]} + } + } ), start=[(0, 0)], end=[(1, 1)], @@ -777,7 +815,13 @@ def test_find_paths_rejects_invalid_algorithm(tmp_path): find_paths( zarr_fp=test_cost_fp, cost_function=json.dumps( - {"cost_layers": [{"layer_name": "test_costs"}]} + { + "routing_options": { + "default": { + "cost_layers": [{"layer_name": "test_costs"}] + } + } + } ), start=[(0, 0)], end=[(1, 1)], diff --git a/tests/python/unit/test_skimage_validate.py b/tests/python/unit/test_skimage_validate.py index 5ffb2859..ef95374a 100644 --- a/tests/python/unit/test_skimage_validate.py +++ b/tests/python/unit/test_skimage_validate.py @@ -40,7 +40,9 @@ def validate_find_paths_single_var(data, start, end, tmp_path, algorithm): ) cost_definition = { - "cost_layers": [{"layer_name": "test_costs"}], + "routing_options": { + "default": {"cost_layers": [{"layer_name": "test_costs"}]} + }, "ignore_invalid_costs": True, } results = find_paths( @@ -86,7 +88,11 @@ def validate_route_finder_single_var(data, start, end, tmp_path, algorithm): test_cost_fp, mode="w", zarr_format=3, consolidated=False ) - cost_definition = {"cost_layers": [{"layer_name": "test_costs"}]} + cost_definition = { + "routing_options": { + "default": {"cost_layers": [{"layer_name": "test_costs"}]} + } + } routing_results = RouteFinder( zarr_fp=test_cost_fp, cost_function=json.dumps(cost_definition), diff --git a/tests/rust/integration_tests.rs b/tests/rust/integration_tests.rs index 1a856d85..c94aa395 100644 --- a/tests/rust/integration_tests.rs +++ b/tests/rust/integration_tests.rs @@ -17,7 +17,7 @@ fn basic_routing_in_data(algorithm: &str) { let end = vec![revrt::ArrayIndex::new_ij(20, 20)]; let result = resolve( layers_path.to_str().expect("test data path is valid UTF-8"), - r#"{"cost_layers": [{"layer_name": "tie_line_costs_102MW"}]}"#, + r#"{"routing_options": {"default": {"cost_layers": [{"layer_name": "tie_line_costs_102MW"}]}}}"#, algorithm, std::slice::from_ref(start), end, @@ -42,10 +42,14 @@ fn basic_routing_in_data_with_friction(algorithm: &str) { let result = resolve( layers_path.to_str().expect("test data path is valid UTF-8"), r#"{ - "cost_layers": [{"layer_name": "tie_line_costs_102MW"}], - "friction_layers": [ - {"multiplier_layer": "transmission_barrier", "multiplier_scalar": 100} - ] + "routing_options": { + "default": { + "cost_layers": [{"layer_name": "tie_line_costs_102MW"}], + "friction_layers": [ + {"multiplier_layer": "transmission_barrier", "multiplier_scalar": 100} + ] + } + } }"#, algorithm, std::slice::from_ref(start), From 760487263279d68105515df8e805d0482a1ae930 Mon Sep 17 00:00:00 2001 From: Paul Date: Mon, 1 Jun 2026 20:06:58 -0600 Subject: [PATCH 38/40] Temp patch for FFI --- crates/revrt/src/ffi/mod.rs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/crates/revrt/src/ffi/mod.rs b/crates/revrt/src/ffi/mod.rs index 00b06418..e35ac679 100644 --- a/crates/revrt/src/ffi/mod.rs +++ b/crates/revrt/src/ffi/mod.rs @@ -25,11 +25,11 @@ impl From<&PyRouteDefinition> for RouteDefinition { route_id: *id, start_inds: start_points .iter() - .map(|(i, j)| ArrayIndex { i: *i, j: *j }) + .map(|(i, j)| ArrayIndex::new_ij(*i, *j)) .collect(), end_inds: end_points .iter() - .map(|(i, j)| ArrayIndex { i: *i, j: *j }) + .map(|(i, j)| ArrayIndex::new_ij(*i, *j)) .collect(), } } @@ -192,9 +192,12 @@ fn find_paths( py_tracing::configure(log_level).map_err(PyErr::from)?; let start: Vec = start .into_iter() - .map(|(i, j)| ArrayIndex { i, j }) + .map(|(i, j)| ArrayIndex::new_ij(i, j)) + .collect(); + let end: Vec = end + .into_iter() + .map(|(i, j)| ArrayIndex::new_ij(i, j)) .collect(); - let end: Vec = end.into_iter().map(|(i, j)| ArrayIndex { i, j }).collect(); let paths = resolve( zarr_fp, &cost_function, @@ -205,6 +208,8 @@ fn find_paths( mem_limit_bytes, ) .map_err(PyErr::from)?; + // TODO: have cost function return the layer mapping so that + // python can perform conversion on it's end Ok(paths.into_iter().map(Into::into).collect()) } From c80e9bbf72c9f7fdac92f3ee11a425fbffe11af2 Mon Sep 17 00:00:00 2001 From: Paul Date: Mon, 1 Jun 2026 20:17:19 -0600 Subject: [PATCH 39/40] disable ruff lint --- revrt/routing/cli/utilities.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/revrt/routing/cli/utilities.py b/revrt/routing/cli/utilities.py index 634d04da..a8598688 100644 --- a/revrt/routing/cli/utilities.py +++ b/revrt/routing/cli/utilities.py @@ -72,7 +72,7 @@ def routing_layer_mover( with tfc as temp_zarr_file_str: logger.debug("Setting swap file location to %r", temp_zarr_file_str) temp_zarr_file = Path(temp_zarr_file_str) - yield temp_zarr_file + yield temp_zarr_file # noqa if not save: return From 6b0196bc514abaa7a98e2098eb31bf18f26e5919 Mon Sep 17 00:00:00 2001 From: Paul Date: Mon, 1 Jun 2026 20:17:51 -0600 Subject: [PATCH 40/40] Linter --- revrt/routing/base.py | 55 +++++++++++++++++++++++-------------------- 1 file changed, 29 insertions(+), 26 deletions(-) diff --git a/revrt/routing/base.py b/revrt/routing/base.py index 517c9f67..3092e202 100644 --- a/revrt/routing/base.py +++ b/revrt/routing/base.py @@ -867,32 +867,7 @@ def _skip_failed_routes(self, routing_results): num_complete += 1 try: route_id, solutions = next(results_iter) - start_points, end_points = self.route_definitions[route_id] - if not solutions: - msg = ( - f"Unable to find route from {start_points} to any of " - f"{end_points} (route ID: {route_id}). Please verify " - "that the start and end points are not separated by " - "hard barriers or invalid cost cells." - ) - logger.error(msg) - continue - - logger.debug( - "Got result from Rust for route_id %d. Processing..." - "\n\t- Start points: %r\n\t- End points: %r", - route_id, - start_points, - end_points, - ) - for indices, optimized_objective, dbl in solutions: - attrs_key = (route_id, indices[0]) - attrs = { - **self.route_attrs.get(attrs_key, self.default_attrs), - "dropped_barrier_layers": json.dumps(dbl), - } - yield indices, optimized_objective, attrs - + yield from self._formatted_solutions(solutions, route_id) time_elapsed = f"{(time.monotonic() - ts) / 60:.2f} minute(s)" logger.info( "%d/%d (%.2f%%) route definitions processed in %s", @@ -908,6 +883,34 @@ def _skip_failed_routes(self, routing_results): logger.info("Routing complete") break + def _formatted_solutions(self, solutions, route_id): + """Format reVRt output solutions and log any failures""" + start_points, end_points = self.route_definitions[route_id] + if not solutions: + msg = ( + f"Unable to find route from {start_points} to any of " + f"{end_points} (route ID: {route_id}). Please verify " + "that the start and end points are not separated by " + "hard barriers or invalid cost cells." + ) + logger.error(msg) + return + + logger.debug( + "Got result from Rust for route_id %d. Processing..." + "\n\t- Start points: %r\n\t- End points: %r", + route_id, + start_points, + end_points, + ) + for indices, optimized_objective, dbl in solutions: + attrs_key = (route_id, indices[0]) + attrs = { + **self.route_attrs.get(attrs_key, self.default_attrs), + "dropped_barrier_layers": json.dumps(dbl), + } + yield indices, optimized_objective, attrs + def _validate_start_points(self, points): """Validate start points by removing cells invalid cost""" points = _get_valid_points(