Generate array of all combinations

I want to generate an array of 3 elements

[alpha, beta, gamma]

Where each of the 3 elements alpha, beta, gamma is between 0 and 1, with a step of 0.01 in each.

I noticed the Iterator::step_by is working with integer increment, so I can do something like:

    for i in (0..=10).step_by(5) {
        println!("{}", i);
    }

But can not do something like:

    for i in (0..=1).step_by(0.01) {
        println!("{}", i);
    }

I've the same code in Julia lang as below:

arr = [[α,β,γ] for α in 0:0.01:1 for β in 0:0.01:1 for γ in 0:0.01:1]

How can I do the same in Rust?

1 Like

The step_by method doesn't do what you think it does. What it does is: given any sequence, create a new sequence that skips items in a way such that it takes every nth item.

It doesn't make sense to talk about the 0.01th item in a sequence which is why it doesn't accept a float.

I recommend doing something like (0..=100).map(|n| n as f64 / 100.0)

3 Likes

Thanks, this solved the issue of step by, so that I can write:

   let alpha: Vec<f64> = (0..=100).map(|n| n as f64 / 100.0).collect();
   let beta: Vec<f64> = (0..=100).map(|n| n as f64 / 100.0).collect();
   let gamma: Vec<f64> = (0..=100).map(|n| n as f64 / 100.0).collect();

Now the issue of all combinations, to simplify it, let's say we have:

   let alpha = vec![1,2];
   let beta = vec![4,8];
   let gamma = vec![3,7];

Then we are having 8 combinations as:

[1, 4, 3]
[1, 4, 7]
[1, 8, 3]
[1, 8, 7]
[2, 4, 3]
[2, 4, 7]
[2, 8, 3]
[2, 8, 7]

How can I get this output with a rust code?

I was able to doit with the below code, but is this the way, or there is a better way:

fn main() {
   let alpha = vec![1,2];
   let beta = vec![4,8];
   let gamma = vec![3,7];
   
   let mut all: Vec<[&i32; 3]> = Vec::new();
   
   for i in &alpha {
        for j in &beta {
            for k in &gamma {
                all.push([i, j, k]);
            }
        }
    }
    
    println!("{:?}", all);

}

Accordingly the answer of my question is, but is this the best code I can write for replacing the single line Julia code:

fn main() {

   let alpha: Vec<f64> = (0..=100).map(|n| n as f64 / 100.0).collect();
   let beta: Vec<f64> = (0..=100).map(|n| n as f64 / 100.0).collect();
   let gamma: Vec<f64> = (0..=100).map(|n| n as f64 / 100.0).collect();
   
   let mut all: Vec<[&f64; 3]> = Vec::new();
   
   for i in &alpha {
        for j in &beta {
            for k in &gamma {
                all.push([i, j, k]);
            }
        }
    }
    
    println!("{:?}", all.len());

}
1 Like

macro_rules! list {
    ($expr:expr; for $x:tt in $range:expr) => {{
        let mut _a: Vec<_> = Vec::new();
        for $x in $range {_a.push($expr);}
        _a
    }};
    ($expr:expr; for $x:tt in $xrange:expr;
        for $y:tt in $yrange:expr
    ) => {{
        let mut _a: Vec<_> = Vec::new();
        for $x in $xrange {
            for $y in $yrange {_a.push($expr)};
        }
        _a
    }};
    ($expr:expr; for $x:tt in $xrange:expr;
        for $y:tt in $yrange:expr; for $z:tt in $zrange:expr
    ) => {{
        let mut _a: Vec<_> = Vec::new();
        for $x in $xrange {
            for $y in $yrange {
                for $z in $zrange {_a.push($expr)};
            }
        }
        _a
    }};
}

pub fn range(start: f64, end: f64, step: f64) -> Vec<f64> {
    let mut buffer: Vec<f64> = Vec::new();
    let mut x = start;
    while x<end {
        buffer.push(x);
        x += step;
    }
    return buffer;
}

fn main() {
    let arr = list![[alpha,beta,gamma];
        for alpha in range(0.0,1.0,0.01);
        for beta  in range(0.0,1.0,0.01);
        for gamma in range(0.0,1.0,0.01)];
    println!("{:?}",arr);
}
1 Like

Addendum.

As far as I can tell, optimizations may be performed as follows:

macro_rules! list {
    ($expr:expr; for $x:tt in $range:expr) => {{
        let _r = $range;
        let mut _a: Vec<_> = Vec::with_capacity(_r.size_hint().0);
        for $x in _r {_a.push($expr);}
        _a
    }};
}

pub struct F64Range {
    x: f64, step: f64, count: usize, len: usize
}

impl Iterator for F64Range {
    type Item = f64;
    #[inline]
    fn next(&mut self) -> Option<Self::Item> {
        let rv = self.x; self.x += self.step; self.count += 1;
        if self.count < self.len {Some(rv)} else {None}
    }
    fn size_hint(&self) -> (usize, Option<usize>) {
        (self.len-self.count, Some(self.len-self.count))
    }
}

pub fn range(start: f64, end: f64, step: f64) -> F64Range {
    let len: usize = ((end-start)/step) as usize+1;
    F64Range{x: start, step, count: 0, len}
}
1 Like

I recommend just using three for loops. There are crates that supply functions for trying combinations, but in this case it's much simpler to just make three for loops.

3 Likes

The nevertheless desirable general variadic implementation of the macro can be achieved with this method "TT muncher". That turned out to work pretty well:

macro_rules! expand_tail {
    ($expr:tt; $v:tt; for $x:tt in $range:expr) => {
        for $x in $range {$v.push($expr);}
    };
    ($expr:tt; $v:tt; for $x:tt in $range:expr; $($tail:tt)*) => {
        for $x in $range {expand_tail!($expr; $v; $($tail)*)}
    };
}

macro_rules! list {
    ($expr:expr; $($tail:tt)*) => {{
        let mut _a = Vec::new();
        expand_tail!{$expr; _a; $($tail)*}
        _a
    }};
}

fn main() {
    let arr = list![[alpha,beta,gamma];
        for alpha in 0..2; for beta in 0..2; for gamma in 0..2];
    println!("{:?}",arr);
}
1 Like

Here's a more declarative way to do this with itertools:

use itertools::Itertools;

use std::iter::once;

fn main() {
    // closure to generate a new iterator as needed without using intermediate Vecs
    let range = || (0..=2).map(|n| n as f64 / 2.0);

    let all: Vec<Vec<f64>> = once(range())
        .chain(once(range()))
        .chain(once(range()))
        .multi_cartesian_product()
        .collect();

    println!("{:?}", all);
}

For Integers you could also have used num_iter for the range generation: http://rust-num.github.io/num/num_iter/fn.range_step_inclusive.html

That doesn't work with floats, though, seemingly for good reason. See: rust - How to do a 'for' loop with boundary values and step as floating point values? - Stack Overflow

3 Likes

Truth be told, I agree with @alice in that I find your solution by far the most obvious and readable, though I would have initialized with Vec::with_capacity(alpha.len() * beta.len() * gamma.len());.

Not much shorter than yours is the curiosity below which uses no macros or external crates; but honestly, I still prefer yours. :wink:
An interesting point I experienced while trying to navigate confusing lifetime errors: we need the move to move a, b, and c in by value (hence the move), but we want to still need to capture alpha, beta, and gamma by reference, which is why I defined them as such so that move captures these references instead of the vectors.

let alpha = &vec![1,2];
let beta = &vec![4,8];
let gamma = &vec![3,7];

let all =
    alpha.iter().flat_map(move |&a|
        beta.iter().flat_map(move |&b|
            gamma.iter().map(move |&c| [a, b, c])))
    .collect::<Vec<_>>();
3 Likes

Indeed true, yet we can use mathematical knowledge to determine the length of the range in before, including some tweaking. Given the range [a,b) we have a + k*step < b for the last index k. Solving the inequality for k results in

k < (b-a)/step    (step>0)

Now let

n := ⌊(b-a)/step⌋.

By definition of the floor function the inequality ⌊x⌋ ≤ x holds. Thus forcing k < n will satisfy the original inequality.

Let's now consider the range [0,1) with step 0.1 and numbers with some small error due to limited precision. By the nature of the floor function there are discontinuities, and they turn out to be at unfortunate points. If step is slightly larger than 0.1 or b is slightly smaller than 1, the number n jumps from 10 to 9. However, this problem may be averted be tweaking the computation to be

n := ⌊(b-a)/step+0.5⌋.

This isn't really a surprise, as round(x):=⌊|x|+0.5⌋ is the standard (naive) method of rounding.

But still be warned if you want the length to have a fixed value in every situation. Then it's better to use the concept of numpy.linspace.

1 Like

Thanks a lot, I learned new things from your answer, apprciated.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.