Feeds:
Posts
Comments

Posts Tagged ‘Rust’

In Part 2 of this series we converted our code to a library, made our initial puzzle into an integration test, and extracted test related parameters and methods from the library.

Now we’re ready to try a new puzzle. This time we’ll expand our solver to handle a slightly more difficult problem – the 8 Queens puzzle.

In the 8 Queens puzzle we wan’t to place 8 chess Queens on a Chess board such that none of them are under attack.

7 Chess Queens in safe positions on a Chess board

According to WikiPedia there are only 92 solutions to this puzzle and once we remove mirrorings and rotations there are only 12 unique solutions.

We start off by figuring out how we’re going to map genes to the problem. One solution that I’ve used before is to assign each square on the 8×8 Chess board a symbol from the 64 symbol set ([a-z][A-Z][0-9]@#) as follows:

board positions labelled first rows then columns

We need to be able to convert a symbol (gene) to a board position. To do that we’ll find its index in the set of genes then convert that index to a row and column, or Point.

    fn to_point(gene: char, gene_set: &str) -> Point {
        let location = gene_set.find(gene);
        assert_eq!(location.is_some(), true);
        let index = location.unwrap() as i32;
        let row = index / 8i32;
        let column = index % 8i32;
        return Point{row: row, col: column};
    }

    struct Point {
        row: i32,
        col: i32
    }

We also need a way to visualize a set of Points (Queens) on a Chess board.

    fn get_board(candidate: &String, gene_set: &str) -> [[char; 8]; 8] {
        let mut board:[[char; 8]; 8] = [['.'; 8]; 8];
        
        for point in candidate.chars().map(|c| to_point(c, gene_set)) {
            board[point.row as usize][point.col as usize] = 'Q';
        }
        board
    }

    fn display_8_queens(candidate: &String, gene_set: &str, start: PreciseTime) {
        let now = PreciseTime::now();
        let elapsed = start.to(now);
        let board:[[char; 8]; 8] = get_board(candidate, gene_set);
        for i in 0..8 {
            let mut row = "".to_string();
            for j in 0..8 {
                row.push(board[i][j]);
                row.push(' ');
            }
            println!("{}", row);
        }
        
        println!("{}\t{}\t{}", candidate, get_8_queens_fitness(&candidate, gene_set), elapsed);
    }

The output of display_8_queens looks like this:

. . Q . . . . .
. . . . . Q . .
. . . . . . . Q
Q . . . . . . .
. . . Q . . . .
. . . . . . Q .
. . . . Q . . .
. Q . . . . . .
0ncJ5yUx        4096    PT0.192306457S

Next we need a way to see 1) if we have 8 Queens on the board, we might not if the same gene appears twice in the generated sequence, and 2) if each Queen is on its own row, column and diagonals.

First the row and column checks. We’ll keep a count of how many rows and columns have exactly one Queen.

    fn get_8_queens_fitness(candidate: &String, gene_set: &str) -> usize {
        let board = get_board(candidate, gene_set);
        
        // count rows with 1 queen
        let indexes: Vec<i32> = (0..8).collect();
        let mut correct_queens_in_row = 0; 
        for i in 0..8 {
            let row_count = indexes.iter()
                .cloned()
                .map(|col| board[i][col as usize])
                .filter(|ch|'Q' == *ch)
                .count();
            if row_count == 1 {
                correct_queens_in_row = correct_queens_in_row + 1;
            }
        }
        let mut correct_queens_in_column = 0; 
        for i in 0..8 {
            let column_count = indexes.iter()
                .cloned()
                .map(|row| board[row as usize][i])
                .filter(|ch|'Q' == *ch)
                .count();
            if column_count == 1 {
                correct_queens_in_column = correct_queens_in_column + 1;
            }
        }

To count the number of diagonals that have exactly one Queen we’ll introduce a generator that creates Points starting from an initial position and then moving by a given row and column offset. First the generator and some tests for it.

    struct Diagonal {
        row: i32,
        col: i32,
        row_offset: i32,
        col_offset: i32
    }

    impl Iterator for Diagonal {
        type Item = Point;
        fn next(&mut self) -> Option<Point> {
            let prev_row = self.row;
            let prev_col = self.col;
            self.row = prev_row + self.row_offset;
            self.col = prev_col + self.col_offset;
            
            // this is an infinite value generator
            Some(Point{row:prev_row,col:prev_col})
        }
    }

    #[test]
    fn test_diagonal_iterator_first_value_returned_should_be_the_start_state() {
        let mut diag = Diagonal {row:5,col:6,row_offset:1,col_offset:1};
        let first = diag.next();
        assert_eq!(true,first.is_some());
        let point = first.unwrap() as Point;
        assert_eq!(point.row,5);
        assert_eq!(point.col,6);
    }
    
    #[test]
    fn test_diagonal_iterator_second_value_returned_should_be_with_offsets_added_to_start_state() {
        let diag = Diagonal {row:5,col:4,row_offset:1,col_offset:-1};
        let first = diag.skip(1).next();
        assert_eq!(true,first.is_some());
        let point = first.unwrap() as Point;
        assert_eq!(point.row,6);
        assert_eq!(point.col,3);
    }

Note: As of this 5/29 the above code does not work with the 1.0.0 release of rustc due to a bug in the compiler… however the nightly build no longer has the bug. Try it on Playpen

And now the diagonal checks…

    let mut correct_queens_in_northeast_diagonal = 0; 
    for i in 0..15 {
        let diag = Diagonal {row:i,col:0,row_offset:-1,col_offset:1};
        let diagonal_count = diag
        .take_while(|point|point.row >= 0 && point.col >= 0)
        .skip_while(|point|point.row >=8 || point.col >=8)
        .take_while(|point|point.col < 8)
        .map(|point| board[point.row as usize][point.col as usize])
        .filter(|ch| 'Q' == *ch)
        .count();
        if diagonal_count == 1 {
        correct_queens_in_northeast_diagonal = correct_queens_in_northeast_diagonal + 1;
        }
    }    

    let mut correct_queens_in_southeast_diagonal = 0; 
    for i in -8..8 {
        let diag = Diagonal {row:i,col:0,row_offset:1,col_offset:1};
        let diagonal_count = diag
        .skip_while(|point|point.row < 0 || point.col < 0)
        .take_while(|point|point.col < 8 && point.row < 8)
        .map(|point| board[point.row as usize][point.col as usize])
        .filter(|ch| 'Q' == *ch)
        .count();
        if diagonal_count == 1 {
        correct_queens_in_southeast_diagonal = correct_queens_in_southeast_diagonal + 1;
        }
    }    

At the end of the method we’ll multiply all the row count values to get the fitness value. Given this the best possible solution would be 8*8*8*8.

    (if correct_queens_in_row == 0 { 1 } else { correct_queens_in_row })
    * (if correct_queens_in_column == 0 { 1 } else { correct_queens_in_column })
    * (if correct_queens_in_northeast_diagonal == 0 { 1 } else { correct_queens_in_northeast_diagonal })
    * (if correct_queens_in_southeast_diagonal == 0 { 1 } else { correct_queens_in_southeast_diagonal })
}

Language aside, rust doesn’t have a trinary operator but it does let you use an if statement inline.

Finally we need a main method to call the solver and verify the results. Once again we’ll create it as an integration test.

    #[test]
    fn test_8_queens() {
        let start = PreciseTime::now();
        let gene_set = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789@#";

        let wrapped_display = |candidate: &String| {display_8_queens(&candidate, gene_set, start);};
        let wrapped_get_fitness = |candidate: &String| -> usize {get_8_queens_fitness(&candidate, gene_set)};
    
        let best = genetic::get_best(wrapped_get_fitness, wrapped_display, 8, 8*8*8*8, gene_set);
        println!("Total time: {}", start.to(PreciseTime::now()));
        let best_fitness = get_8_queens_fitness(&best, gene_set);
        assert_eq!(best_fitness,8*8*8*8);
    }

When we run that test we may get a successful result like the following.

cargo test test_8_queens -- --nocapture
     Running target\debug\genetic-debe16ff205aab6a.exe

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured

     Running target\debug\lib_8_queens_tests-afc3deed67cad86e.exe

running 1 test
. . Q . Q . . .
Q Q . . . . . .
Q . . . Q . . .
. . . . . . . .
. . . . . . . .
. . . . . . Q .
. . . . . . . .
. . Q . . . . .
ujcieqU6        120     PT0.003442522S
. . Q . Q . . .
Q . . . . . . .
Q . . . Q . . .
. . . . . . . .
. . . . . . . .
. . . . . . Q .
. Q . . . . . .
. . Q . . . . .
uXcieqU6        192     PT0.004552093S
. . Q . Q . . .
Q . . . . . . .
Q . . . Q . . .
. . . . . Q . .
. . . . . . . .
. . . . . . Q .
. Q . . . . . .
. . . . . . . .
uXcieqUD        480     PT0.005474583S
. . Q . Q . . .
Q . . . . . . .
. . . . Q . . .
Q . . . . Q . .
. . . . . . . .
. . . . . . Q .
. Q . . . . . .
. . . . . . . .
uXcieyUD        640     PT0.006346324S
. . Q . Q . . .
Q . . . . . . .
. . . . Q . . .
Q . . . . . . .
. . . . . . . .
. . . . . . Q .
. Q . . . . . .
Q . . . . . . .
uXcieyU4        648     PT0.007207255S
. . Q . Q . . .
Q . . . . . . .
. . . . . . . .
Q . . . . . . .
. . . . . . . .
. . . . . . Q .
. Q . . . . . .
Q . . . . . . .
eXcieyU4        700     PT0.009275650S
. . Q . Q . . .
Q . . . . . . .
. . . . . . . .
Q . . . . . . .
. . . . . . . .
. . . . . Q Q .
. Q . . . . . .
. . . . . . . .
eXcieyUT        735     PT0.010128473S
. . Q . Q . . .
Q . . . . . . .
. . . . . . . .
Q . . . . . . .
. . . . . . . .
. Q . . . Q Q .
. Q . . . . . .
. . . . . . . .
eXciPyUT        768     PT0.010938355S
. . Q . Q . . .
Q . . . . . . .
. . . . . . . .
Q . . . . . . .
. . . . . . . .
. . . . . Q Q .
. Q . . . . . .
. . . . . . . Q
eXci#yUT        1152    PT0.011762050S
. . Q . Q . . .
Q . . . . . . .
. . . . . . . .
Q . . . . . . .
. . . . . . . .
. . . . . . Q .
. Q . . . . . .
. . . . . . . Q
eXci#yU#        1225    PT0.013189927S
. . Q . Q . . .
Q . . . . . . .
. . . . . . . Q
Q . . . . . . .
. . . . . . . .
. . . . . . Q .
. Q . . . . . .
. . . . . . . Q
eXci#yUx        1536    PT0.014965841S
. . Q . Q . . .
. . . . . . . .
. . . . . . . Q
Q . . . . . . .
. . . Q . . . .
. . . . . . Q .
. Q . . . . . .
. . . . . . . Q
eXcJ#yUx        1728    PT0.021601343S
. . Q . Q . . .
. . . . . . . Q
. . . . . . . Q
Q . . . . . . .
. . . Q . . . .
. . . . . . Q .
. . . . . . . .
. . . . . . . Q
epcJ#yUx        1920    PT0.023504280S
. . Q . . . . .
. . . . . . . Q
. . . . . . . Q
Q . . . . . . .
. . . Q . . . .
. . . . . . Q .
. . . . Q . . .
. . . . . . . Q
0pcJ#yUx        2560    PT0.030906422S
. . Q . . . . .
. . . . . Q . .
. . . . . . . Q
Q . . . . . . .
. . . Q . . . .
. . . . . . Q .
. . . . Q . . .
. . . . . . . Q
0ncJ#yUx        3072    PT0.166375236S
. . Q . . . . .
. . . . . Q . .
. . . . . . . Q
Q . . . . . . .
. . . Q . . . .
. . . . . . Q .
. . . . Q . . .
. Q . . . . . .
0ncJ5yUx        4096    PT0.192306457S
Total time: PT0.193881176S
test tests::test_8_queens ... ok

But the odds are against it… That’s because the Mutation genetic strategy can’t always solve this problem. For our solver to be able to find a solution every time we’re going to have to introduce a new strategy. That is the subject of Part 4.

The source code to this point is available on Github if you want to experiment.

Advertisements

Read Full Post »

In Part 1 we saw how a no-knowledge genetic string duplicator could be written in Rust. One problem with that implementation is the solver received parameters that it didn’t care about (start time) and shouldn’t have access to (target) to be truly no-knowledge.  In this post we’ll solve fix those issues and, in addition, turn our string duplication implementation into an integration test.

Things learned:

  • how to wrap a library in a module
  • how to setup integration tests
  • how to pass closures (lambdas) as functions to another function

First, we’ll wrap the code in a module.

pub mod genetic {
    use rand::{thread_rng, sample, Rng};
    use time::PreciseTime;

    pub fn get_best(

Two things to note here. First, we explicitly mark the module and exposed methods as public with pub and second, the use statements belong inside the module.

Next we’ll create a tests folder parallel to the src folder. This is where cargo expects to find integration tests. Create a file called lib_tests.rs and move the string duplication specific code from main.rs to it.

extern crate genetic;
extern crate time;

#[cfg(test)]
mod tests {

    use time::PreciseTime;    
    use genetic::*;

    #[test]
    fn test_duplicate_string() {
        let start = PreciseTime::now();
        let gene_set = " abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!.";
        let target = "Not all those who wander are lost.";
        let best = genetic::get_best(get_fitness, display, target, gene_set, target.len(), start);
        println!("{}", best);
        println!("Total time: {}", start.to(PreciseTime::now()));
        assert!(best == target);
    }
    
    fn get_fitness(candidate: &String, target: &str) -> usize {
        let different_count = target.chars()
            .zip(candidate.chars())
            .filter(|&(a, b)| a != b)
            .count();

        target.len() - different_count
    }

    fn display(candidate: &String, target: &str, start: PreciseTime) {
        let now = PreciseTime::now();
        let elapsed = start.to(now);
        println!("{}\t{}\t{}", candidate, get_fitness(&candidate, target),elapsed);
    }
}

Notice that we’re referencing our genetic module as an external crate (1) because our integration tests won’t be compiled into our library binary, that we’ve added a module with a conditional compile attribute (4,5), and added a test attribute to our main method (10), which as been renamed to test_duplicate_string (11). Also note that we now have an assertion to verify that the solver was able to duplicate the target string (18).

Next we need to rename main.rs to lib.rs because we’re changing what we’re building from a executable to a library.

The code change above is available on Github.

The tests can be run from the commandline via cargo test as follows:

cargo test
   Compiling genetic v0.0.2 (file:.../genetic)
     Running target\debug\genetic-d3350e5be5a2acc2.exe

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured

     Running target\debug\lib_tests-30b315b873936a67.exe

running 1 test
test tests::test_duplicate_string ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured

   Doc-tests genetic

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured

You can see that it ran our test but you can’t see the output. If you want to force cargo to write the output use cargo test -- --nocapture

The next problem we want to tackle in the library code is it has access to things it shouldn’t (the target string), and receives parameters it doesn’t care about (start time). To resolve this we’re going to introduce closures around our functions so that the methods in the test module still have access to the parameters they need, but the genetic library doesn’t need to know about them.

We start off by creating closures for the display and get_fitness functions in test_duplicate_string.

    let wrapped_display = |candidate: &String| {
        display(&candidate, target, start);
    };
    let wrapped_get_fitness = |candidate: &String| -> usize {
        get_fitness(&candidate, target)
    };

This allows us to stop passing the target and start time to get_best.

    let best = genetic::get_best(
        wrapped_get_fitness, 
        wrapped_display, 
        target.len(), 
        gene_set);

On the library side we have to change the function parameters so that it can receive the closure.

    pub fn get_best<F,D>(
        get_fitness: F, 
        display: D, 
        length: usize, 
        gene_set: &str) -> String
        where F : Fn(&String)->usize,
        D : Fn(&String)
    {

The F and D aliases let us pass closures as functions. The syntax is a bit more convoluted than other languages I’ve used but not difficult to follow once you’ve seen a working example.

The updated source code is available on Github.

Go on to Part 3.

Read Full Post »

In this series I’ll be building out a simple genetic solver in Rust, a new programming language for me. Building a genetic solver is my preferred programming problem for learning new programming languages.  I’ve previously solved this problem in C# and used it to learn to code in golang. If you are new to genetic algorithms start with the C# post.

This was my first program in Rust and from scratch it took about four hours to work through issues.

Things I learned:

  • Rust’s compiler is particular about variable naming, preferring underscores to camel case.
  • There’s a lot of syntax, particularly around loaning items to called functions, but it isn’t that hard to follow if you’ve had experience with C/C++ and templated types.
  • The line that returns a value from a function cannot end with a semicolon.
  • The compiler does a great job of explaining what it is expecting.  Just take it slow and build up the program one function at a time.
  • The Rust install doesn’t necessarily contain everything you might need. On windows, I had to install MingW to get gcc so it can compile the code in the time crate.

OK, let’s go. We’ll start off with a standard set of genes and a target string:

    let gene_set = " abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!.";
    let target = "Not all those who wander are lost.";

Next we need a way to generate a random gene sequence (parent) from the gene set.

fn generate_parent(gene_set: &str, length: usize) -> String {
    let mut rng = thread_rng();
    let sample = sample(&mut rng, gene_set.chars(), length);
    sample.into_iter().collect()
}

Note, sample is a function provided by the rand crate.  It returns N items randomly selected from the input.

Next we need to calculate a fitness value based on the number of differences between a candidate string and the target string.

fn get_fitness(candidate: &String, target: &str) -> usize {
    let different_count = target.chars()
      .zip(candidate.chars())
      .filter(|&(a, b)| a != b)
      .count();
 
    target.len() - different_count
}

It is nice that map/reduce type functions are available out of the box, like in C#.

We also need a way to produce a new (child) string by mutating an existing (parent) string.  The point is to create a copy of the parent then replace 1 character (gene) in the copy with a randomly selected one from the set of valid genes.

fn mutate_parent(parent: &String, gene_set: &str) -> String {
    let mut rng = thread_rng();
    let gene_index = rng.gen::<usize>() % gene_set.len();
    let parent_index = rng.gen::<usize>() % parent.len();
    let mut candidate = String::with_capacity(parent.len());
 
    if parent_index > 0 {
        candidate.push_str(&parent[..parent_index]);
    }
    candidate.push_str(&gene_set[gene_index..(1+gene_index)]);
    if parent_index+1 < parent.len() {
        candidate.push_str(&parent[parent_index+1..]);
    }
    candidate
}

There’s probably a simpler way to do that in Rust along the lines of:

  1. copy the parent
  2. select a random location in the parent to mutate
  3. select a random gene to insert
  4. copy[random_location] = new_gene

Now we need a way to display a gene sequence, its fitness value and how much time has elapsed.

fn display(candidate: &String, target: &str, start: time::PreciseTime) {
    let now = PreciseTime::now();
    let elapsed = start.to(now);
    println!("{}\t{}\t{}", candidate, get_fitness(&candidate, target), elapsed);
}

The heart of the genetic solver is a loop that uses the functions above to generate a candidate gene sequence, compare it to the previous best, and randomly mutate it until all the genes match those in the target.

fn get_best(get_fitness: fn(&String,&str) -> usize, 
  display: fn(&String, &str, start: time::PreciseTime), 
  target: &str, 
  gene_set: &str, 
  length: usize, 
  start: time::PreciseTime) -> String {

    let mut best_parent = generate_parent(gene_set, length);
    let mut best_fitness = get_fitness(&best_parent, target);
 
    while best_fitness < length {
        let child = mutate_parent(&best_parent, gene_set);
        let fitness = get_fitness(&child, target);
        if fitness > best_fitness {
            best_fitness = fitness;
            best_parent = child;
            display(&best_parent, target, start);
        }
    }

    best_parent 
}

Yes, I know we could just call the functions instead of passing them in but this demonstrates that capability in the language and it is a feature we will need in order to use different fitness functions and display methods in a more complex solver.

The main function brings all the parts together:

extern crate time;
extern crate rand;

use rand::{thread_rng, sample, Rng};
use time::PreciseTime;

fn main() {
    let start = PreciseTime::now();
    let gene_set = " abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!.";
    let target = "Not all those who wander are lost.";
    let best = get_best(get_fitness, display, target, gene_set, target.len(), start);
    println!("{}", best);
    println!("Total time: {}", start.to(PreciseTime::now()));
}

...

Get the source here.

Because the time crate is not pre-compiled you may (on Windows) need to install MingW as appropriate for your OS (32 or 64bit).

Then run the program from the commandline with:

cargo run

The first time it runs it will download and compile the dependencies it needs.  Nice feature.

Sample output:

>cargo run
 Updating registry `https://github.com/rust-lang/crates.io-index`
 Downloading rand v0.3.8
 Downloading time v0.1.25
 Downloading libc v0.1.7
 Downloading gcc v0.3.5
 Compiling gcc v0.3.5
 Compiling rand v0.3.8
 Compiling time v0.1.25
 Compiling genetic v0.0.1 (.../projects/rust/genetic)
 Running `target\debug\genetic.exe`
 aH.RefKhPjklmOopq!stuMwxSrAVCDEWG 1 PT0.004136558S
 aH.aefKhPjklmOopq!stuMwxSrAVCDEWG 2 PT0.006234092S
 aH.aefKhPjklmOopq!stdMwxSrAVCDEWG 3 PT0.006508939S
 aH.aefKhPoklmOopq!stdMwxSrAVCDEWG 4 PT0.006637123S
 aH.aefKhPoklmOopq!stdMwxSrAVCDEtG 5 PT0.007313462S
 aH.aefKhPoklmOopq!stdMwxSreVCDEtG 6 PT0.008119910S
 aH.aefKhPokemOopq!stdMwxSreVCDEtG 7 PT0.011582439S
 aH.aefKhPokemOopq!stdewxSreVCDEtG 8 PT0.012396586S
 aH.alfKhPokemOopq!stdewxSreVCDEtG 9 PT0.014519526S
 oH.alfKhPokemOopq!stdewxSreVCDEtG 10 PT0.015515364S
 oH.alfKhPokemwopq!stdewxSreVCDEtG 11 PT0.020154268S
 oH.alfKtPokemwopq!stdewxSreVCDEtG 12 PT0.020608496S
 oH.alfKthokemwopq!stdewxSreVCDEtG 13 PT0.021233638S
 oH.alfKthokemwooq!stdewxSreVCDEtG 14 PT0.021521572S
 oH.alfKthokemwooq!sndewxSreVCDEtG 15 PT0.023080962S
NoH.alfKthokemwooq!sndewxSreVCDEtG 16 PT0.023575224S
NoH.alfKthokemwooq!sndewxSre CDEtG 17 PT0.024708100S
NoH.alfKthokemwoo !sndewxSre CDEtG 18 PT0.025637729S
NoH alfKthokemwoo !sndewxSre CDEtG 19 PT0.028930115S
NoH alfKthokemwoo !sndew Sre CDEtG 20 PT0.029574888S
NoH alfKthokemwoo !sndew are CDEtG 21 PT0.030039510S
NoH alf thokemwoo !sndew are CDEtG 22 PT0.034410881S
Not alf thokemwoo !sndew are CDEtG 23 PT0.039302690S
Not alf thosemwoo !sndew are CDEtG 24 PT0.040969862S
Not alf thosemwoo !sndew are CDstG 25 PT0.041442952S
Not alf those woo !sndew are CDstG 26 PT0.043314527S
Not all those woo !sndew are CDstG 27 PT0.045133751S
Not all those woo !sndew are CDst. 28 PT0.047369478S
Not all those woo !sndew are lDst. 29 PT0.050106782S
Not all those woo wsndew are lDst. 30 PT0.057051472S
Not all those woo wsndew are lost. 31 PT0.062051064S
Not all those woo wsnder are lost. 32 PT0.066566787S
Not all those who wsnder are lost. 33 PT0.069746386S
Not all those who wander are lost. 34 PT0.081900460S
Not all those who wander are lost.
Total time: PT0.082663410S

Wow! That’s a lot faster than the go version!

Go on to Part 2.

Read Full Post »

%d bloggers like this: