Crates.io | orx-funvec |
lib.rs | orx-funvec |
version | 0.1.6 |
source | src |
created_at | 2023-12-12 21:51:05.418285 |
updated_at | 2024-11-19 02:51:31.68048 |
description | Traits to unify access to elements of n-dimensional vectors which are particularly useful in algorithms requiring both flexibility through abstraction over inputs and performance through monomorphization. |
homepage | |
repository | https://github.com/orxfun/orx-closure/ |
max_upload_size | |
id | 1067112 |
size | 104,186 |
The experimental ideas here have been extended and transformed into vector traits defined in the orx-v crate, which supports functional vectors in addition to others. Therefore, this crate will be discontinued, but the idea will be continued in the orx-v crate.
Traits to unify access to elements of n-dimensional vectors which are particularly useful in algorithms requiring both flexibility through abstraction over inputs and performance through monomorphization.
trait FunVec<const DIM: usize, T>
represents DIM
dimensional vectors of T
requiring only the following method to be implemented:
fn at<Idx: IntoIndex<DIM>>(&self, index: Idx) -> Option<T>
FunVec
is different from, a generalization of, multi dimensional vectors due to the following:
Additionally, the trait provides with, auto implements, the iter_over
method which allows to iterate over values of the vector at the given indices.
The crate also provides the reference returning counterpart FunVecRef<const DIM: usize, T>
requiring the method fn ref_at<Idx: IntoIndex<DIM>>(&self, index: Idx) -> Option<&T>
.
This crate provides implementations for a wide range of types which are useful in algorithms. For instance, the following implementations are available:
trait FunVec<1, T> |
trait FunVec<2, T> |
---|---|
Vec<T> |
|
[T; N] |
|
HashMap<usize, T> | BTreeMap<usize, T> |
HashMap<(usize, usize), T> | BTreeMap<[usize, usize], T> |
Closure<Capture, usize, T> |
Closure<Capture, (usize, usize), T> |
Box<dyn Fn(usize) -> T> |
Box<dyn Fn([usize, usize] -> T) |
You may notice the pattern in the indices; (usize, usize)
or [usize, usize]
can be used interchangeable as they both implement IntoIndex<2>
. And as we move to higher dimensions, only the index dimension changes.
However, the recursive implementations to allow for compositions are also available. For instance all of the following types implement FunVec<2, T>
for any V1
provided that V1
implements FunVec<1, T>
:
Vec<V1>
[V1; N]
HashMap<usize, V1>
| BTreeMap<usize, V1>
Closure<Capture, usize, V1>
Box<dyn Fn(usize) -> V1>
Lastly, ScalarAsVec<T>
and EmptyVec<T>
implement FunVec<D, T>
for any dimension D
. These turn out to be useful common special cases.
Finally, the following implementations are optionally provided through features:
ndarray
by impl_ndarray
feature,indexmap
by impl_indexmap
feature,smallvec
by impl_smallvec
feature,impl_all
feature.Implementing the trait for a new type is straightforward, requiring only to implement at
method. Please see section C5 for an example.
The motivation of the create is demonstrated by a use case in the following example.
Assume we need to solve a network problem, namely minimum cost network flow (mcnf) problem (wikipedia). In the example, we ignore the graph input. Instead, we focus on the following inputs:
The mcnf problem provides a certain level of abstraction over inputs. For instance:
Abstraction over the inputs is powerful since it allows to implement a generic mcnf solver without the need to make assumptions on the concrete input types.
Below we implement our fake solver generic over the input types.
use orx_funvec::*;
const N: usize = 4;
type Unit = i32;
#[derive(derive_new::new)]
struct FakeResult {
sum_demands: i32,
sum_costs: i32,
sum_capacities: i32,
}
#[derive(derive_new::new)]
struct FakeMcnfSolver<Demands, Costs, Capacities>
where
Demands: FunVec<1, Unit>,
Costs: FunVec<2, Unit>,
Capacities: FunVec<2, Unit>,
{
demands: Demands,
costs: Costs,
capacities: Capacities,
}
impl<Demands, Costs, Capacities> FakeMcnfSolver<Demands, Costs, Capacities>
where
Demands: FunVec<1, Unit>,
Costs: FunVec<2, Unit>,
Capacities: FunVec<2, Unit>,
{
fn fake_solve(&self) -> FakeResult {
let sum_demands = self
.demands
.iter_over(0..N)
.flatten()
.filter(|x| x > &0)
.sum();
let mut sum_costs = 0;
let mut sum_capacities = 0;
for i in 0..N {
for j in 0..N {
if let Some(cost) = self.costs.at([i, j]) {
sum_costs += cost;
}
if let Some(capacity) = self.capacities.at((i, j)) {
sum_capacities += capacity;
}
}
}
FakeResult::new(sum_demands, sum_costs, sum_capacities)
}
}
In the below example, we use our generic solver with:
Closure<_, usize, Unit>
),Vec<Vec<Unit>>
),Box<dyn Fn((usize, usize)) -> Unit>
).use orx_closure::Capture;
fn some_if_not_self_edge(ij: (usize, usize), value: i32) -> Option<i32> {
if ij.0 == ij.1 {
None
} else {
Some(value)
}
}
// mcnf problem with a single source and sink
let source = 0;
let sink = 2;
let demand = 10;
// demands vector as a no-box orx_closure::Closure
let demands =
Capture((source, sink, demand)).fun(|(s, t, d), i: usize| match (i == *s, i == *t) {
(true, _) => Some(*d),
(_, true) => Some(-*d),
_ => None,
});
// complete cost matrix
let costs = vec![
vec![0, 1, 2, 3],
vec![2, 0, 2, 2],
vec![7, 10, 0, 9],
vec![1, 1, 1, 0],
];
// capacities matrix as a box dyn Fn
let capacities: Box<dyn Fn((usize, usize)) -> Option<i32>> =
Box::new(|ij: (usize, usize)| some_if_not_self_edge(ij, 100));
// simulate & assert
let solver = FakeMcnfSolver::new(demands, costs, capacities);
let result = solver.fake_solve();
assert_eq!(10, result.sum_demands);
assert_eq!(41, result.sum_costs);
assert_eq!((N * (N - 1) * 100) as i32, result.sum_capacities);
In the second example variant, we use our solver with:
Closure<_, usize, Unit>
),Closure<_, (usize, usize), Unit>
),Vec<HashMap<usize, Unit>>
).use orx_closure::Capture;
use std::collections::HashMap;
fn get_euclidean_distance(location1: (f64, f64), location2: (f64, f64)) -> i32 {
let (x1, y1) = location1;
let (x2, y2) = location2;
(f64::powf(x1 - x2, 2.0) + f64::powf(y1 - y2, 2.0)).sqrt() as i32
}
// multi-commodity mcnf problem
struct Commodity {
origin: usize,
destination: usize,
amount: Unit,
}
let commodities = vec![
Commodity {
origin: 0,
destination: 2,
amount: 10,
},
Commodity {
origin: 1,
destination: 3,
amount: 20,
},
];
// demands vector as a no-box orx_closure::Closure capturing a reference of commodities collection
let demands = Capture(&commodities).fun(|com, i: usize| {
Some(
com.iter()
.map(|c| {
if c.origin == i {
c.amount
} else if c.destination == i {
-c.amount
} else {
0
}
})
.sum::<i32>(),
)
});
// costs computed as Euclidean distances of node coordinates
let locations = vec![(0.0, 3.0), (3.0, 5.0), (7.0, 2.0), (1.0, 1.0)];
let costs = Capture(locations).fun(|loc, (i, j): (usize, usize)| {
loc.get(i)
.and_then(|l1| loc.get(j).map(|l2| (l1, l2)))
.map(|(l1, l2)| get_euclidean_distance(*l1, *l2))
});
// capacities defined as a Vec of HashMap to take advantage of sparsity in the graph
let capacities = vec![
HashMap::from_iter([(1, 100), (3, 200)].into_iter()),
HashMap::from_iter([(3, 300)].into_iter()),
HashMap::from_iter([(0, 400), (3, 500)].into_iter()),
HashMap::new(),
];
// simulate & assert
let solver = FakeMcnfSolver::new(demands, costs, capacities);
let result = solver.fake_solve();
assert_eq!(30, result.sum_demands);
assert_eq!(54, result.sum_costs);
assert_eq!(1500, result.sum_capacities);
Next, we solve a shortest distance problem with the generic solver:
Closure<_, usize, Unit>
),Closure<_, (usize, usize), Unit>
),at
calls will be replaced by the inlined value (ScalarAsVec<Unit>
).let source = 3;
let sink = 1;
// demands vector as a no-box orx_closure::Closure
let demands = Capture((source, sink)).fun(|(s, t), i: usize| match (i == *s, i == *t) {
(true, _) => Some(1),
(_, true) => Some(-1),
_ => None,
});
// costs computed as Euclidean distances of node coordinates
let locations = vec![(0.0, 3.0), (3.0, 5.0), (7.0, 2.0), (1.0, 1.0)];
let costs = Capture(locations).fun(|loc, (i, j): (usize, usize)| {
loc.get(i)
.and_then(|l1| loc.get(j).map(|l2| (l1, l2)))
.map(|(l1, l2)| get_euclidean_distance(*l1, *l2))
});
// uniform capacities for all edges
let capacities = ScalarAsVec(1);
// simulate & assert
let solver = FakeMcnfSolver::new(demands, costs, capacities);
let result = solver.fake_solve();
assert_eq!(1, result.sum_demands);
assert_eq!(54, result.sum_costs);
assert_eq!((N * N) as i32, result.sum_capacities);
So far in the above examples, we have made use of the available implementations. Here, we will see an example where we implement FunVec
for our own type.
Consider a case where computing the cost between two nodes is expensive. We might be required to solve shortest path problem for each pair. We want to be lazy in this computation because the solver will not need need the cost between each pair. Therefore, we decide to compute the costs on the fly when requested. On the other hand, we do not want to repeat the computation when the solver requests the same pair multiple times, and hence, we will cache already computed costs. In brief, we want a costs matrix which is a shortest path algorithm combined with an internal cache.
type Unit = i32;
use std::{cell::RefCell, collections::HashMap};
#[derive(Default)]
struct DistanceProvider {
// graph: skipping for the brevity, but we'd probably hold a graph ref to compute the shortest distances
cached: RefCell<HashMap<(usize, usize), Option<Unit>>>,
}
impl DistanceProvider {
fn distance(&self, from: usize, to: usize) -> Option<Unit> {
if let Some(cached) = self.cached.borrow().get(&(from, to)) {
return *cached;
}
let distance = self.compute_shortest_distance(from, to);
self.cached.borrow_mut().insert((from, to), distance);
distance
}
/// Computes shortest distance between `from` and `to` returns `None` if `to` is unreachable from `from`.
fn compute_shortest_distance(&self, _from: usize, _to: usize) -> Option<Unit> {
Some(1) // expensive computation!
}
}
Now, can we use our DistanceProvider
as the 'costs' input of the FakeMcnfSolver
? Yes. All we need is to implement the at
method of FunVec<2, Unit>
as below.
impl FunVec<2, Unit> for DistanceProvider {
fn at<Idx: IntoIndex<2>>(&self, index: Idx) -> Option<Unit> {
let [from, to] = index.into_index();
self.distance(from, to)
}
}
This abstraction allows us to keep the algorithm intact while we can make significant changes on how we represent the inputs.
let source = 3;
let sink = 1;
// demands vector as a no-box orx_closure::Closure
let demands = Capture((source, sink)).fun(|(s, t), i: usize| match (i == *s, i == *t) {
(true, _) => Some(1),
(_, true) => Some(-1),
_ => None,
});
// our custom caching distance provider
let costs = DistanceProvider::default();
// uniform capacities for all edges
let capacities = ScalarAsVec(1);
// simulate & assert
let solver = FakeMcnfSolver::new(demands, costs, capacities);
let result = solver.fake_solve();
assert_eq!(1, result.sum_demands);
assert_eq!(4 * 4, result.sum_costs);
assert_eq!((N * N) as i32, result.sum_capacities);