diff --git a/.gitignore b/.gitignore index b6e4761..5e36f5e 100644 --- a/.gitignore +++ b/.gitignore @@ -127,3 +127,6 @@ dmypy.json # Pyre type checker .pyre/ + +# image results +*.png \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..73f69e0 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..11a5d8e --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +main.py \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..f02d5e7 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..49a9249 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/random_walk.iml b/.idea/random_walk.iml new file mode 100644 index 0000000..d0876a7 --- /dev/null +++ b/.idea/random_walk.iml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/README.md b/README.md index a53d95c..458adac 100644 --- a/README.md +++ b/README.md @@ -1,55 +1,71 @@ # Random Walk Simulation -This is a **group exercise**, so you should be working in pairs of two students. It's **30% of your final grade**. +## Write an extended random walk program -The Goal is to **practise writing readable, maintainable and reliable code collaboratively.** +In this repo you find a basic implementation of a [random walk simulation](https://en.wikipedia.org/wiki/Random_walk) in 2-dimensional space taken from [this blogpost](https://www.geeksforgeeks.org/random-walk-implementation-python/). Running the code yields an image which shows the path of the random walk. -## Group Exercise +The result will be directly shown to the user and additionally saved as png. -1. One student of your group forks the code from [https://github.com/advanced-geoscripting-2021/random_walker.git](https://github.com/advanced-geoscripting-2021/random_walker.git) +![random_walk](rand_walk_5_100.png) +start: point; end: cross -2. This student invites the other student as a collaborator to the forked repository. Now you can both work on the code. +An example for the landscape raster walker +![img_1.png](img_1.png) -3. Adapt the code to fulfil the requirements (see below). +## Setting up the project +1. clone repository +2. setup a virtual environment +3. activate the virtual environment +4. install required libs and packages locally (see requirements.txt) -4. Code review: Each group reviews the code of another group. +#### For the advanced geoscripting course: +- activate the corresponding virtual environment +- run ``conda install -c conda-forge click`` +- run ``pip install --editable .`` -5. Improve your code based on the review you got. +## How to run the tool +```` +$ random_walker -## Write an extended random walk program - -In this repo you find a basic implementation of a [random walk simulation](https://en.wikipedia.org/wiki/Random_walk) in 2-dimensional space taken from [this blogpost](https://www.geeksforgeeks.org/random-walk-implementation-python/). Running the code yields an image which shows the path of the random walk. +Usage: random_walker [OPTIONS] COMMAND [ARGS]... -![random_walk](rand_walk_100000.png) +Options: + --verbose TEXT Will print verbose messages. + --help Show this message and exit. -The program works but it is not very readable. In addition, you should **extend the program based on the requirements listed below. +Commands: + run execute command to generate random walkers + ```` -**Remember to apply the best practices in scientific computing** to make the code more readable, maintainable, reusable and efficient. +### How to use the run method +```` +Usage: random_walker run [OPTIONS] -### Minimum requirements: + execute command to generate random walkers -Extend the program so the following requirements are met: +Options: + -ts, --total_steps INTEGER Specify the number of total steps for each + random walker, Default is 10,000, Minimum 100 -1. The program should be able to simulate multiple random walkers. -2. The program should be executable from the command line. -3. The user should be able to specify the number of random walkers through a command line parameter. -4. Document the dependencies and instructions of how to run the program in your README.md. + -tw, --total_walkers INTEGER Specify the number of total walkers, Default + is 1, Minimum is 1 -### Additional requirements: + -ss, --step_size INTEGER Specify the size of the steps taken, Default + is 1 -1. Create three different types of walkers, e.g. a "fast walker" which has a bigger step size. -2. Add a "landscape" in which the random walkers are walking in which contains obstacles which the walkers cannot cross (e.g. a lake) -3. Invent and implement another functionality of your own. + -l, --landscape BOOLEAN Specify whether a grid landscape exists as + base layer or not, Default is False -Be creative here! :) + -sp, --start_point BOOLEAN Specify whether the walkers shall start from + the same point or not, Default is False -## Code Review + -mp, --mov_pattern BOOLEAN Specify the neighborhood movement pattern, + False is Neumann, True is Moor -Review the code of another group: (tuesday afternoon or wednesday morning) + --help Show this message and exit. -1. Does it work properly? Try to make it fail! -2. Are the best-practices implemented in the code? -3. Is the documentation clear? -4. Can you adapt the code easily? E.g. try to create a new type of random walker which moves two cells per iteration. +```` +## Example +``random_walker run --total_steps 10000 --step_size 2 -l False`` diff --git a/main.py b/main.py index 9aac7f0..7ac0ba9 100644 --- a/main.py +++ b/main.py @@ -4,37 +4,147 @@ # Python code for 2D random walk. # Source: https://www.geeksforgeeks.org/random-walk-implementation-python/ -import numpy -import matplotlib.pyplot as plt -import random - -# defining the number of steps -n = 100000 - -# creating two array for containing x and y coordinate -# of size equals to the number of size and filled up with 0's -x = numpy.zeros(n) -y = numpy.zeros(n) - -# filling the coordinates with random variables -for i in range(1, n): - val = random.randint(1, 4) - if val == 1: - x[i] = x[i - 1] + 1 - y[i] = y[i - 1] - elif val == 2: - x[i] = x[i - 1] - 1 - y[i] = y[i - 1] - elif val == 3: - x[i] = x[i - 1] - y[i] = y[i - 1] + 1 + +import click +import raster_walker as rw +import vector_walker as vw + + +# user input – click +_total_steps_option = [ + click.option( + "--total_steps", + "-ts", + default=10000, + type=int, + help="Specify the number of total steps for each random walker," + "Default is 10,000, Minimum 100", + ) +] + +_total_walkers_option = [ + click.option( + "--total_walkers", + "-tw", + default=1, + type=int, + help="Specify the number of total walkers, Default is 1, Minimum is 1", + ) +] + +_step_size_option = [ + click.option( + "--step_size", + "-ss", + default=1, + type=int, + help="Specify the size of the steps taken, Default is 1", + ) +] + +_landscape_option = [ + click.option( + "--landscape", + "-l", + default=False, + type=bool, + help="Specify whether a grid landscape exists as base layer or not, Default is False", + ) +] + +_start_point_option = [ + click.option( + "--start_point", + "-sp", + default=False, + type=bool, + help="Specify whether the walkers shall start from the same point or not, Default is False", + ) +] + +_mov_pattern_option = [ + click.option( + "--mov_pattern", + "-mp", + default=False, + type=bool, + help="Specify the neighborhood movement pattern, False is Neumann, True is Moor", + ) +] + + +def add_options(options): + """Functions adds options to cli.""" + + def _add_options(func): + for option in reversed(options): + func = option(func) + return func + return _add_options + + +@click.group() +@click.option('--verbose', '-v', is_flag=False, help="Will print verbose messages.") +def cli(verbose: bool) -> None: + if verbose: + click.echo("We are in the verbose mode. Which does not make any" + "difference right now.. but hey, have fun!") + + +@cli.command() +@add_options(_total_steps_option) +@add_options(_total_walkers_option) +@add_options(_step_size_option) +@add_options(_landscape_option) +@add_options(_start_point_option) +@add_options(_mov_pattern_option) +def run( + total_steps: int, + total_walkers: int, + step_size: int, + landscape: bool, + start_point: bool, + mov_pattern: bool +) -> None: + """ execute command to generate random walkers """ + run_random_walkers(total_steps, total_walkers, step_size, landscape, start_point, mov_pattern) + + +def run_random_walkers(total_steps, total_walkers, step_size, landscape, diff_start, mov_pattern): + """ + executes the random walker tool based on input data + :param total_steps: number of total steps of the random walker + :param total_walkers: number of walkers + :param step_size: step size for each step + :param landscape: boolean, if True, 2D area with obstacles is generated as base layer + :param diff_start: boolean, if True, different start points for each walker + :param mov_pattern: boolean, if True, Moor'sche neighboorhood is used, else Neumann + :return: + """ + + # adjust wrong input + total_steps = max(total_steps, 100) + total_walkers = max(total_walkers, 1) + + # diverted because of the completely different implementation methods -> + # could be done better in the future + if landscape: + # percentage of how much space obstacles shall block + fill_percentage = 0.1 + landscape_raster = rw.create_raster(total_steps, fill_percentage) + walk = rw.r_walker(total_steps, landscape_raster) + # plot landscape raster with obstacles and walker + rw.plot_raster(walk, total_steps) else: - x[i] = x[i - 1] - y[i] = y[i - 1] - 1 + # creating two arrays for containing x and y coordinate + # of size equals to the number of size and filled up with 0's + x_arr, y_arr = vw.create_walking_space(total_steps) + + # multiple walkers + list_x, list_y = vw.multiple_v_walkers(x_arr, y_arr, total_steps, total_walkers, + step_size, diff_start, mov_pattern) + vw.plot_v_walkers(total_steps, total_walkers, list_x, list_y) -# plotting the walk -plt.title("Random Walk ($n = " + str(n) + "$ steps)") -plt.plot(x, y) -plt.savefig("./rand_walk_{}.png".format(n)) -plt.show() \ No newline at end of file +if __name__ == "__main__": + run_random_walkers(100, 5, 1, True, True, True) diff --git a/rand_walk_100000.png b/rand_walk_100000.png deleted file mode 100644 index 5582dd8..0000000 Binary files a/rand_walk_100000.png and /dev/null differ diff --git a/rand_walk_5_100.png b/rand_walk_5_100.png new file mode 100644 index 0000000..005f474 Binary files /dev/null and b/rand_walk_5_100.png differ diff --git a/raster_walker.py b/raster_walker.py new file mode 100644 index 0000000..b89a9d1 --- /dev/null +++ b/raster_walker.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" Rasterized Walker""" + + +import random +import math +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.colors import ListedColormap +from matplotlib.patches import Patch + + +def check_landscape(landscape, position): + """ + Checks if next position of walker intersects given landscape + :param landscape: + :param position: + :return: + """ + # implement endless border -> reach border on the right, come in on the left + # like the good old snake + if abs(position[0]) >= landscape.shape[0]: + position[0] = position[0] % landscape.shape[0] + if abs(position[1]) >= landscape.shape[1]: + position[1] = position[1] % landscape.shape[1] + + # check if the given position is an obstacle or the starting point + if landscape[position[0], position[1]] != 1 and landscape[position[0], position[1]] != 3: + return True + return False + + +def raster_one_step(direction, curr_pos, future_pos): + """ + Alters given position based on given direction + :param direction: String, celestial direction + :param curr_pos: the current position + :param future_pos: future position + :return: updated future position + """ + if direction == "EAST": + future_pos[1] = curr_pos[1] + 1 + elif direction == "WEST": + future_pos[1] = curr_pos[1] - 1 + elif direction == "NORTH": + future_pos[0] = curr_pos[0] + 1 + else: + future_pos[0] = curr_pos[0] - 1 + + return future_pos + + +def r_walker(total_steps, landscape, direction_set=("NORTH", "SOUTH", "EAST", "WEST")): + """ + Random walker on 2D array with an obstacle with step size 1 + :param total_steps: integer specifying the number of steps by the walker + :param landscape: 2D numpy array with some landscape features + :param direction_set: defines a set of directions, default values North, South, East, West + :return: x, y numpy arrays + """ + + # start upper left corner + curr_pos = [0, 0] + future_pos = [0, 0] + + # give the starting position another value for plotting it later + landscape[curr_pos[0], curr_pos[1]] = 3 + + for step in range(0, total_steps): + + check = False + + while not check: + direction = random.choice(direction_set) + future_pos = raster_one_step(direction, curr_pos, future_pos) + check = check_landscape(landscape, future_pos) + + landscape[future_pos[0], future_pos[1]] = 2 + curr_pos = future_pos + return landscape + + +def create_raster(total_steps: int, fill=0.1): + """ + generates x,y 1D arrays with zeros for total_steps or + generates landscape 2D array with obstacles if landscape is true + :param total_steps: total count of steps + :param fill: how much of the area of the 2D array shall be filled with obstacles (percentage) + :return: + """ + + # side_length as square root of the total steps + side_length = int(math.sqrt(total_steps)) + + # create 2D array + arr = np.zeros((side_length, side_length)) + + total_area = arr.size + fill_area = int(total_area * fill) + if fill_area % 2 == 1: + fill_area += 1 + + # get length of one size of the square + side_length_obstacle = int(math.sqrt(fill_area)) + + centre = side_length / 2 + + # centre with x, y + upper_left = (int(centre - side_length_obstacle / 2), int(centre - side_length_obstacle / 2)) + + # places an obstacle as square in the middle of the landscape arr + for row in range(side_length_obstacle): + for col in range(side_length_obstacle): + arr[upper_left[0] + col, upper_left[1] + row] = 1 + return arr + + +def plot_raster(arr, total_steps): + """ + Plots landscape with the path of the random walker + :param total_steps: total number of steps + :param arr: raster array of landscape + :return: + """ + cmap = ListedColormap(["grey", "black", "darkgreen", "red"]) + + fig, axs = plt.subplots(figsize=(10, 5)) + + axs.imshow(arr, cmap=cmap) + + axs.set_title("Random Walk (Number of walkers = 1, total steps: " + str(total_steps)) + + # Add a legend for labels + legend_labels = {"black": "obstacle", "darkgreen": "path", "red": "start point"} + + patches = [Patch(color=color, label=label) + for color, label in legend_labels.items()] + + axs.legend(handles=patches, + bbox_to_anchor=(1.35, 1), + facecolor="white") + + axs.set_axis_off() + plt.savefig(".\\rand_walk_raster_{}.png".format(total_steps)) + plt.show() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..4f3d2cd --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +matplotlib~=3.3.4 +numpy~=1.20.2 +click~=7.1.2 +setuptools~=52.0.0 \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..f1c8b79 --- /dev/null +++ b/setup.py @@ -0,0 +1,14 @@ +from setuptools import setup + +setup( + name="random_walker", + version="0.1", + py_modules=["main"], + install_requires=[ + "Click", + ], + entry_points=''' + [console_scripts] + random_walker=main:cli + ''' +) \ No newline at end of file diff --git a/test_raster_walkers.py b/test_raster_walkers.py new file mode 100644 index 0000000..5e35812 --- /dev/null +++ b/test_raster_walkers.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" tests the raster_walker.py functionality""" + + +import numpy as np +import raster_walker as rw + + +def test_create_raster(): + """ Checks if raster creation works and implements the obstacle correctly """ + created_raster = rw.create_raster(100) + expected_raster = np.zeros((10, 10)) + expected_raster[3:6, 3:6] = 1 + assert np.array_equal(created_raster, expected_raster) + + +def test_raster_one_step(): + """ Checks if raster one step returns the correct changes """ + direction_set = ("NORTH", "SOUTH", "EAST", "WEST") + curr_pos = [0, 0] + future_pos = [0, 0] + + pos_north = rw.raster_one_step(direction_set[0], curr_pos, future_pos) + future_pos = [0, 0] + pos_south = rw.raster_one_step(direction_set[1], curr_pos, future_pos) + future_pos = [0, 0] + pos_east = rw.raster_one_step(direction_set[2], curr_pos, future_pos) + future_pos = [0, 0] + pos_west = rw.raster_one_step(direction_set[3], curr_pos, future_pos) + + expected_north = [1, 0] + expected_south = [-1, 0] + expected_east = [0, 1] + expected_west = [0, -1] + + assert pos_north == expected_north + assert pos_south == expected_south + assert pos_east == expected_east + assert pos_west == expected_west + + +def test_check_landscape(): + """ Checks if raster obstacle check is working correct """ + + raster = np.zeros((4, 4)) + raster[1:3, 1:3] = 1 + raster[3, 3] = 3 + position1 = [0, 0] + position2 = [2, 2] + position3 = [0, -1] + position4 = [-1, 0] + position5 = [4, 0] + position6 = [0, 4] + position7 = [3, 3] + + assert rw.check_landscape(raster, position1) is True + assert rw.check_landscape(raster, position2) is False + assert rw.check_landscape(raster, position3) is True + assert rw.check_landscape(raster, position4) is True + assert rw.check_landscape(raster, position5) is True + assert rw.check_landscape(raster, position6) is True + assert rw.check_landscape(raster, position7) is False + + +if __name__ == "__main__": + test_raster_one_step() + test_create_raster() + test_check_landscape() diff --git a/test_vector_walker.py b/test_vector_walker.py new file mode 100644 index 0000000..3eccc4e --- /dev/null +++ b/test_vector_walker.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +"""Unit testing of walkers""" + +import raster_walker as rw +import vector_walker as vw + + +def test_walker_next(): + # define initial position and direction + start = (2, 2) + total_steps = 1 + x, y = vw.create_walking_space(total_steps) + direction = "EAST" + step_size = 1 + expected_new_x = 3 + expected_new_y = 2 + + # execute the nextstep function + new_x, new_y = vw.next_step(x, y, 0, direction, step_size) + + + # assert + assert new_x[0] + start[0] == expected_new_x + assert new_y[0] + start[1] == expected_new_y diff --git a/vector_walker.py b/vector_walker.py new file mode 100644 index 0000000..8e0d54b --- /dev/null +++ b/vector_walker.py @@ -0,0 +1,185 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +"""Vectorized Walker""" + +import random +import math as m +import matplotlib.pyplot as plt +import numpy as np + + +def next_step(x_arr, y_arr, pos, direction, step_size): + """ + Returns the next step for a random walker + :param x_arr: numpy array of the walker's path – x-coordinates + :param y_arr: numpy array of the walker's path – y-coordinates + :param pos: which step is walker doing + :param direction: direction of the step + :param step_size: size of the step + :return: updated arrays of the path + """ + # go east + if direction == "EAST": + x_arr[pos] = x_arr[pos - 1] + step_size + y_arr[pos] = y_arr[pos - 1] + # go west + elif direction == "WEST": + x_arr[pos] = x_arr[pos - 1] - step_size + y_arr[pos] = y_arr[pos - 1] + # go north + elif direction == "NORTH": + x_arr[pos] = x_arr[pos - 1] + y_arr[pos] = y_arr[pos - 1] + step_size + # go south + elif direction == "SOUTH": + x_arr[pos] = x_arr[pos - 1] + y_arr[pos] = y_arr[pos - 1] - step_size + # go northeast + elif direction == "NORTHEAST": + x_arr[pos] = x_arr[pos - 1] + step_size + y_arr[pos] = y_arr[pos - 1] + step_size + # go northwest + elif direction == "NORTHWEST": + x_arr[pos] = x_arr[pos - 1] - step_size + y_arr[pos] = y_arr[pos - 1] + step_size + # go southeast + elif direction == "SOUTHEAST": + x_arr[pos] = x_arr[pos - 1] + step_size + y_arr[pos] = y_arr[pos - 1] - step_size + # go southwest + elif direction == "SOUTHWEST": + x_arr[pos] = x_arr[pos - 1] - step_size + y_arr[pos] = y_arr[pos - 1] - step_size + return x_arr, y_arr + + +def different_start_pos(total_steps): + """ + Returns a random starting position of a walker + :param total_steps: based on number of steps the start shift is chosen + :return: tuple of random integer coordinates + """ + if total_steps <= 10: + start_shift = 10 + else: + start_shift = int(m.sqrt(total_steps)) + return random.randint(-start_shift, start_shift), random.randint(-start_shift, start_shift) + + +def get_random_direction(direction_set): + """ + Returns a random direction + :param direction_set: von Neumann or Moor'sche neighborhood + :return: random direction (f.e. 'NORTH') + """ + return random.choice(direction_set) + + +def create_walking_space(total_steps): + """ + Creating empty arrays for x,y-coordinates + :param total_steps: number of steps + :return: numpy arrays + """ + # for random walker without landscape + x_arr, y_arr = np.zeros(total_steps).astype(int), np.zeros(total_steps).astype(int) + return x_arr, y_arr + + +def v_walker(x_arr, y_arr, total_steps, diff_start, step_size=1, + mov_pattern=False): + """ + Normal random walker with step size 1 + :param diff_start: if True, walker starts walking + from a different position, else starts at (0,0) + :param total_steps: number of steps + :param step_size: defines the size of the steps + :param x_arr: empty numpy array consisting of n zeros + :param y_arr: empty numpy array consisting of n zeros + :param mov_pattern: boolean, if True, Moor'sche neighboorhood is used, else Neumann + :return: x, y numpy arrays + """ + + # checks which movement set the walker should get + # Neumann or Moor + if not mov_pattern: + # von Neumann neighborhood + direction_set = ("NORTH", "SOUTH", "EAST", "WEST") + else: + # Moor'sche neighborhood + direction_set = ("NORTH", "SOUTH", "EAST", "WEST", + "NORTHWEST", "NORTHEAST", "SOUTHWEST", "SOUTHEAST") + + # get random start position if that is wanted + if diff_start is True: + start = (different_start_pos(total_steps)) + else: + start = (0, 0) + # for the total number of steps, calculate walker movement randomly + for pos in range(1, total_steps): + direction = get_random_direction(direction_set) + x_arr, y_arr = next_step(x_arr, y_arr, pos, direction, step_size) + return x_arr + start[0], y_arr + start[1] + + +def multiple_v_walkers(x_arr, y_arr, total_steps, total_walkers, step_size, + diff_start, mov_pattern): + """ + Generates paths for multiple walkers + :param x_arr: np.array of x-coordinates (input: zeros) + :param y_arr: np.array of y-coordinates (input: zeros) + :param total_steps: number of steps of individual walker + :param total_walkers: number of walkers + :param step_size: defines the size of the steps + :param diff_start: bool value – if True, walkers start from different position + if False, walkers start from the same position + :param mov_pattern: boolean, if True, Moor'sche neighboorhood is used, else Neumann + :return: lists of x and y coordinates + """ + # create empty lists + x_list = [] + y_list = [] + for _ in range(total_walkers): + # call walker function to generate array of x and y coordinates of one walker + x_axis, y_axis = v_walker(x_arr, y_arr, total_steps, diff_start, step_size, mov_pattern) + # append to list + x_list.append(x_axis) + y_list.append(y_axis) + # set input arrays back to zeros + x_arr, y_arr = create_walking_space(total_steps) + return x_list, y_list + + +# plotting the walk +def plot_v_walkers(total_steps, total_walkers, x_list, y_list): + """ + Generates plot of walker(s) and saves figure as PNG file. + :param total_steps: Number of steps (needed for a figure title) + :param total_walkers: Number of walkers (needed for a figure title) + :param x_list: List of x-coordinates of walker(s) position + :param y_list: List of y-coordinates of walker(s) position + :return: none + """ + # set figure and axis + fig = plt.figure(figsize=(5, 5), dpi=200) + axes = fig.add_subplot(111) + # create list of unique colors + color = iter(plt.cm.rainbow(np.linspace(0, 1, total_walkers))) + for wal in range(total_walkers): + col = next(color) + path_x = x_list[wal] + path_y = y_list[wal] + # plot vertices, path, start position and end position + axes.scatter(path_x, path_y, color=col, alpha=0.25, s=1) + axes.plot(path_x, path_y, color=col, alpha=0.25, lw=2, label='%s. walker' % (wal+1)) + axes.plot(path_x[0], path_y[0], color=col, marker='o') + axes.plot(path_x[-1], path_y[-1], color=col, marker='+') + axes.axis('equal') + plt.xlabel('x') + plt.ylabel('x') + plt.legend() + plt.tight_layout() + plt.title("Random Walk (Number of walkers = " + str(total_walkers) + + ", $n = " + str(total_steps) + "$ steps)") + plt.savefig(".\\rand_walk_{}_{}.png".format(total_walkers, total_steps)) + plt.show()