Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
071450d
ArrayIndex stores z value
ppinchuk May 11, 2026
b64191d
Update grid shape to be 3D
ppinchuk May 11, 2026
72cc16e
Array index update
ppinchuk May 11, 2026
0f17a6e
New method
ppinchuk May 11, 2026
b6079ec
UPdate layout to track options dimension
ppinchuk May 16, 2026
8d87163
Derived writer now pulls from 3 dimensions
ppinchuk May 16, 2026
e8a8384
Pass correct function args
ppinchuk May 16, 2026
4e961a6
Reader is now option-aware
ppinchuk May 16, 2026
2fe04c8
Rename struct
ppinchuk May 16, 2026
044bc74
Rename var
ppinchuk May 16, 2026
6b006fc
Add `get_cell_cost` for derived data reader
ppinchuk May 16, 2026
b1d164d
Add comment
ppinchuk May 16, 2026
c6ccdf7
Dataset can now get center cell cost
ppinchuk May 16, 2026
ca9b87d
Allow dataset to get non-cost layer cells
ppinchuk May 16, 2026
2ad080c
Add comment
ppinchuk May 16, 2026
7274b17
Grid indexer is now 3D
ppinchuk May 16, 2026
53ad2fc
Add helper methods
ppinchuk May 16, 2026
3e87c43
Add `allowed_states_at`
ppinchuk May 16, 2026
d9f2368
Successors now determined by driver layer and can include transitions…
ppinchuk May 16, 2026
b25983d
Return an iter now
ppinchuk May 16, 2026
29312de
Drop routes with barriered or driver excluded start points
ppinchuk May 16, 2026
844e48c
Tests for new functionality
ppinchuk May 16, 2026
37e5864
Add feature
ppinchuk May 16, 2026
c633d73
Merge remote-tracking branch 'origin/main' into pp/rust_z_level
ppinchuk May 18, 2026
9e6051b
Merge remote-tracking branch 'origin/main' into pp/rust_z_level
ppinchuk May 19, 2026
0d3360c
minor bug fix
ppinchuk May 19, 2026
aadc274
Minor docstring
ppinchuk May 19, 2026
baf434e
Merge remote-tracking branch 'origin/main' into pp/rust_z_level
ppinchuk May 21, 2026
db43375
Add inputs file
ppinchuk Jun 1, 2026
fb01cea
Formatting
ppinchuk Jun 1, 2026
b85b143
input now in charge of creating the driver rule set
ppinchuk Jun 1, 2026
fb51df1
inputs in charge of creating pieces
ppinchuk Jun 1, 2026
60eb47a
Add components
ppinchuk Jun 1, 2026
17f8bf2
Cost function uses new components
ppinchuk Jun 1, 2026
72f6dc8
Use new components
ppinchuk Jun 1, 2026
2f71281
Pull directly from cost function
ppinchuk Jun 1, 2026
29235d0
Rename module
ppinchuk Jun 1, 2026
d376868
Move to new module
ppinchuk Jun 1, 2026
554a685
Privatize components and methods
ppinchuk Jun 2, 2026
b043765
Minimal wire up on python side
ppinchuk Jun 2, 2026
7604872
Temp patch for FFI
ppinchuk Jun 2, 2026
c80e9bb
disable ruff lint
ppinchuk Jun 2, 2026
6b0196b
Linter
ppinchuk Jun 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand Down
7 changes: 5 additions & 2 deletions crates/cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,14 @@ 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(
Expand Down
24 changes: 12 additions & 12 deletions crates/revrt/benches/standard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]),
)
})
});
Expand All @@ -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)]),
)
})
});
Expand All @@ -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::<Vec<_>>(),
),
black_box(vec![ArrayIndex::new(10, 50)]),
black_box(vec![ArrayIndex::new_ij(10, 50)]),
)
})
});
Expand All @@ -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::<Vec<_>>(),
),
black_box(vec![ArrayIndex::new(50, 50)]),
black_box(vec![ArrayIndex::new_ij(50, 50)]),
)
})
});
Expand All @@ -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)]),
)
})
});
Expand All @@ -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)]),
)
})
},
Expand Down
16 changes: 10 additions & 6 deletions crates/revrt/src/benchmark.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
254 changes: 254 additions & 0 deletions crates/revrt/src/cost/components.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
//! Cost function components

use core::f32;
use derive_builder::Builder;
use std::collections::HashMap;
Comment thread
ppinchuk marked this conversation as resolved.

#[derive(Clone, Debug, Default)]
pub(crate) struct TransitionCostTable {
pub(super) default: f32,
pub(super) pairwise: HashMap<(u32, u32), f32>,
}

impl TransitionCostTable {
pub(super) 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(super) default: Vec<Option<f32>>,
pub(super) zones: Vec<DriverZoneRule>,
}

impl DriverRuleSet {
pub(super) fn new(default: Vec<Option<f32>>, zones: Vec<DriverZoneRule>) -> Self {
Self { default, zones }
}

pub(crate) fn multiplier<F>(&self, option: u32, mut layer_value: F) -> Option<f32>
where
F: FnMut(&str) -> Option<f32>,
{
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(super) struct DriverZoneRule {
pub(super) layer_name: String,
pub(super) operator: BarrierOperator,
pub(super) threshold: f32,
pub(super) options: HashMap<u32, Option<f32>>,
}

impl DriverZoneRule {
pub(super) fn new(
layer_name: String,
operator: BarrierOperator,
threshold: f32,
options: HashMap<u32, Option<f32>>,
) -> 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(super) 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(super) struct CostLayer {
pub(crate) layer_name: String,
#[builder(setter(strip_option), default)]
pub(super) multiplier_scalar: Option<f32>,
#[builder(setter(strip_option, into), default)]
pub(super) multiplier_layer: Option<String>,
#[builder(setter(strip_option), default)]
pub(super) is_invariant: Option<bool>,
#[builder(default, setter(skip))]
#[serde(skip)]
pub(super) option: u32,
}

impl CostLayer {
pub(super) 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(super) struct FrictionLayer {
pub(super) multiplier_layer: String,
#[builder(setter(strip_option), default)]
pub(super) multiplier_scalar: Option<f32>,
#[serde(skip)]
pub(super) option: u32,
}

impl FrictionLayer {
pub(super) fn new(
multiplier_layer: String,
multiplier_scalar: Option<f32>,
option: u32,
) -> Self {
Self {
multiplier_layer,
multiplier_scalar,
option,
}
}
}

#[derive(Clone, Debug, serde::Deserialize)]
pub(crate) struct BarrierLayer {
pub(crate) layer_name: String,
pub(super) barrier_operator: BarrierOperator,
pub(super) barrier_threshold: f32,
pub(super) barrier_importance: Option<u32>,
#[serde(skip)]
pub(super) option: u32,
}

impl BarrierLayer {
pub(super) fn importance(&self) -> Option<u32> {
self.barrier_importance
}

pub(super) 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));
}
}
Loading
Loading