diff --git a/examples/play_model.py b/examples/play_model.py index d54decd8950a7e0ef8fa67f987f458b6b0fed005..6a67397ea4ba8d8906ce62b1a8d21327c247a3e0 100644 --- a/examples/play_model.py +++ b/examples/play_model.py @@ -1,17 +1,16 @@ from flatland.envs.rail_env import RailEnv, random_rail_generator # from flatland.core.env_observation_builder import TreeObsForRailEnv from flatland.utils.rendertools import RenderTool -from flatland.utils.render_qt import QtRailRender from flatland.baselines.dueling_double_dqn import Agent from collections import deque import torch import random import numpy as np import matplotlib.pyplot as plt -import redis +import time -def main(): +def main(render=True, delay=0.0): random.seed(1) np.random.seed(1) @@ -28,12 +27,13 @@ def main(): 0.0] # Case 7 - dead end # Example generate a random rail - env = RailEnv(width=7, - height=7, + env = RailEnv(width=15, + height=15, rail_generator=random_rail_generator(cell_type_relative_proportion=transition_probability), - number_of_agents=1) - env_renderer = RenderTool(env, gl="QT") - #env_renderer = QtRailRender(env) + number_of_agents=5) + + if render: + env_renderer = RenderTool(env, gl="QT") plt.figure(figsize=(5,5)) # fRedis = redis.Redis() @@ -52,7 +52,7 @@ def main(): dones_list = [] action_prob = [0]*4 agent = Agent(state_size, action_size, "FC", 0) - agent.qnetwork_local.load_state_dict(torch.load('../flatland/baselines/Nets/avoid_checkpoint9900.pth')) + # agent.qnetwork_local.load_state_dict(torch.load('../flatland/baselines/Nets/avoid_checkpoint9900.pth')) def max_lt(seq, val): """ @@ -67,6 +67,8 @@ def main(): idx -= 1 return None + iFrame = 0 + tStart = time.time() for trials in range(1, n_trials + 1): # Reset environment @@ -102,7 +104,13 @@ def main(): agent.step(obs[a], action_dict[a], all_rewards[a], next_obs[a], done[a]) score += all_rewards[a] - env_renderer.renderEnv(show=True, frames=True, iEpisode=trials, iStep=step) + if render: + env_renderer.renderEnv(show=True, frames=True, iEpisode=trials, iStep=step) + if delay > 0: + time.sleep(delay) + + iFrame += 1 + obs = next_obs.copy() if done['__all__']: @@ -116,8 +124,8 @@ def main(): scores.append(np.mean(scores_window)) dones_list.append((np.mean(done_window))) - print('\rTraining {} Agents.\tEpisode {}\tAverage Score: {:.0f}\tDones: {:.2f}%' + - '\tEpsilon: {:.2f} \t Action Probabilities: \t {}'.format( + print(('\rTraining {} Agents.\tEpisode {}\tAverage Score: {:.0f}\tDones: {:.2f}%' + + '\tEpsilon: {:.2f} \t Action Probabilities: \t {}').format( env.number_of_agents, trials, np.mean(scores_window), @@ -125,16 +133,15 @@ def main(): eps, action_prob/np.sum(action_prob)), end=" ") if trials % 100 == 0: - - print( - '\rTraining {} Agents.\tEpisode {}\tAverage Score: {:.0f}\tDones: {:.2f}%\tEpsilon: {:.2f} \t Action Probabilities: \t {}'.format( + tNow = time.time() + rFps = iFrame / (tNow - tStart) + print(('\rTraining {} Agents.\tEpisode {}\tAverage Score: {:.0f}\tDones: {:.2f}%' + + '\tEpsilon: {:.2f} fps: {:.2f} \t Action Probabilities: \t {}').format( env.number_of_agents, trials, - np.mean( - scores_window), - 100 * np.mean( - done_window), - eps, action_prob / np.sum(action_prob))) + np.mean(scores_window), + 100 * np.mean(done_window), + eps, rFps, action_prob / np.sum(action_prob))) torch.save(agent.qnetwork_local.state_dict(), '../flatland/baselines/Nets/avoid_checkpoint' + str(trials) + '.pth') action_prob = [1]*4 diff --git a/flatland/core/env_observation_builder.py b/flatland/core/env_observation_builder.py index 86485ec2c068ea410d5d27997f6f037d3aab6c23..8737862b60d9c330cb95e7679b3e39c2ad897da6 100644 --- a/flatland/core/env_observation_builder.py +++ b/flatland/core/env_observation_builder.py @@ -114,7 +114,7 @@ class TreeObsForRailEnv(ObservationBuilder): nodes_queue.append(n) if len(valid_neighbors) > 0: - max_distance = max(max_distance, node[3]+1) + max_distance = max(max_distance, node[3] + 1) return max_distance @@ -129,7 +129,7 @@ class TreeObsForRailEnv(ObservationBuilder): if enforce_target_direction >= 0: # The agent must land into the current cell with orientation `enforce_target_direction'. # This is only possible if the agent has arrived from the cell in the opposite direction! - possible_directions = [(enforce_target_direction+2) % 4] + possible_directions = [(enforce_target_direction + 2) % 4] for neigh_direction in possible_directions: new_cell = self._new_position(position, neigh_direction) @@ -137,7 +137,7 @@ class TreeObsForRailEnv(ObservationBuilder): if new_cell[0] >= 0 and new_cell[0] < self.env.height and \ new_cell[1] >= 0 and new_cell[1] < self.env.width: - desired_movement_from_new_cell = (neigh_direction+2) % 4 + desired_movement_from_new_cell = (neigh_direction + 2) % 4 """ # Is the next cell a dead-end? @@ -166,7 +166,7 @@ class TreeObsForRailEnv(ObservationBuilder): movement = (desired_movement_from_new_cell+2) % 4 """ new_distance = min(self.distance_map[target_nr, new_cell[0], new_cell[1], agent_orientation], - current_distance+1) + current_distance + 1) neighbors.append((new_cell[0], new_cell[1], agent_orientation, new_distance)) self.distance_map[target_nr, new_cell[0], new_cell[1], agent_orientation] = new_distance @@ -177,11 +177,11 @@ class TreeObsForRailEnv(ObservationBuilder): Utility function that converts a compass movement over a 2D grid to new positions (r, c). """ if movement == 0: # NORTH - return (position[0]-1, position[1]) + return (position[0] - 1, position[1]) elif movement == 1: # EAST return (position[0], position[1] + 1) elif movement == 2: # SOUTH - return (position[0]+1, position[1]) + return (position[0] + 1, position[1]) elif movement == 3: # WEST return (position[0], position[1] - 1) @@ -241,7 +241,7 @@ class TreeObsForRailEnv(ObservationBuilder): # Start from the current orientation, and see which transitions are available; # organize them as [left, forward, right, back], relative to the current orientation - for branch_direction in [(orientation+4+i) % 4 for i in range(-1, 3)]: + for branch_direction in [(orientation + 4 + i) % 4 for i in range(-1, 3)]: if self.env.rail.get_transition((position[0], position[1], orientation), branch_direction): new_cell = self._new_position(position, branch_direction) @@ -253,7 +253,7 @@ class TreeObsForRailEnv(ObservationBuilder): for i in range(self.max_depth): num_cells_to_fill_in += pow4 pow4 *= 4 - observation = observation + [-np.inf, -np.inf, -np.inf, -np.inf, -np.inf]*num_cells_to_fill_in + observation = observation + [-np.inf, -np.inf, -np.inf, -np.inf, -np.inf] * num_cells_to_fill_in return observation @@ -262,7 +262,7 @@ class TreeObsForRailEnv(ObservationBuilder): Utility function to compute tree-based observations. """ # [Recursive branch opened] - if depth >= self.max_depth+1: + if depth >= self.max_depth + 1: return [] # Continue along direction until next switch or @@ -356,7 +356,7 @@ class TreeObsForRailEnv(ObservationBuilder): observation = [0, 1 if other_target_encountered else 0, 1 if other_agent_encountered else 0, - root_observation[3]+num_steps, + root_observation[3] + num_steps, 0] elif last_isTerminal: @@ -369,7 +369,7 @@ class TreeObsForRailEnv(ObservationBuilder): observation = [0, 1 if other_target_encountered else 0, 1 if other_agent_encountered else 0, - root_observation[3]+num_steps, + root_observation[3] + num_steps, self.distance_map[handle, position[0], position[1], direction]] # ############################# @@ -379,18 +379,18 @@ class TreeObsForRailEnv(ObservationBuilder): # Start from the current orientation, and see which transitions are available; # organize them as [left, forward, right, back], relative to the current orientation - for branch_direction in [(direction+4+i) % 4 for i in range(-1, 3)]: + for branch_direction in [(direction + 4 + i) % 4 for i in range(-1, 3)]: if last_isDeadEnd and self.env.rail.get_transition((position[0], position[1], direction), - (branch_direction+2) % 4): + (branch_direction + 2) % 4): # Swap forward and back in case of dead-end, so that an agent can learn that going forward takes # it back - new_cell = self._new_position(position, (branch_direction+2) % 4) + new_cell = self._new_position(position, (branch_direction + 2) % 4) branch_observation = self._explore_branch(handle, new_cell, - (branch_direction+2) % 4, + (branch_direction + 2) % 4, new_root_observation, - depth+1) + depth + 1) observation = observation + branch_observation elif last_isSwitch and self.env.rail.get_transition((position[0], position[1], direction), @@ -401,16 +401,16 @@ class TreeObsForRailEnv(ObservationBuilder): new_cell, branch_direction, new_root_observation, - depth+1) + depth + 1) observation = observation + branch_observation else: num_cells_to_fill_in = 0 pow4 = 1 - for i in range(self.max_depth-depth): + for i in range(self.max_depth - depth): num_cells_to_fill_in += pow4 pow4 *= 4 - observation = observation + [-np.inf, -np.inf, -np.inf, -np.inf, -np.inf]*num_cells_to_fill_in + observation = observation + [-np.inf, -np.inf, -np.inf, -np.inf, -np.inf] * num_cells_to_fill_in return observation @@ -422,7 +422,7 @@ class TreeObsForRailEnv(ObservationBuilder): return depth = 0 - tmp = len(tree)/num_features_per_node-1 + tmp = len(tree) / num_features_per_node - 1 pow4 = 4 while tmp > 0: tmp -= pow4 @@ -431,15 +431,15 @@ class TreeObsForRailEnv(ObservationBuilder): prompt_ = ['L:', 'F:', 'R:', 'B:'] - print(" "*current_depth + prompt, tree[0:num_features_per_node]) - child_size = (len(tree)-num_features_per_node)//4 + print(" " * current_depth + prompt, tree[0:num_features_per_node]) + child_size = (len(tree) - num_features_per_node) // 4 for children in range(4): - child_tree = tree[(num_features_per_node+children*child_size): - (num_features_per_node+(children+1)*child_size)] + child_tree = tree[(num_features_per_node + children * child_size): + (num_features_per_node + (children + 1) * child_size)] self.util_print_obs_subtree(child_tree, num_features_per_node, prompt=prompt_[children], - current_depth=current_depth+1) + current_depth=current_depth + 1) class GlobalObsForRailEnv(ObservationBuilder): diff --git a/flatland/core/transitions.py b/flatland/core/transitions.py index eb4cb8e394c2effd6e5ba1bfcedf381281ea9388..a8cb8d6f49157bafbb65551b53c6612c45565c88 100644 --- a/flatland/core/transitions.py +++ b/flatland/core/transitions.py @@ -180,7 +180,7 @@ class Grid4Transitions(Transitions): List of the validity of transitions in the cell. """ - bits = (cell_transition >> ((3-orientation)*4)) + bits = (cell_transition >> ((3 - orientation) * 4)) return ((bits >> 3) & 1, (bits >> 2) & 1, (bits >> 1) & 1, (bits) & 1) def set_transitions(self, cell_transition, orientation, new_transitions): @@ -208,7 +208,7 @@ class Grid4Transitions(Transitions): `orientation'. """ - mask = (1 << ((4-orientation)*4)) - (1 << ((3-orientation)*4)) + mask = (1 << ((4 - orientation) * 4)) - (1 << ((3 - orientation) * 4)) negmask = ~mask new_transitions = \ @@ -217,9 +217,7 @@ class Grid4Transitions(Transitions): (new_transitions[2] & 1) << 1 | \ (new_transitions[3] & 1) - cell_transition = \ - (cell_transition & negmask) | \ - (new_transitions << ((3-orientation)*4)) + cell_transition = (cell_transition & negmask) | (new_transitions << ((3 - orientation) * 4)) return cell_transition @@ -245,8 +243,7 @@ class Grid4Transitions(Transitions): Validity of the requested transition: 0/1 allowed/not allowed. """ - return ((cell_transition >> ((4-1-orientation) * 4)) >> - (4-1-direction)) & 1 + return ((cell_transition >> ((4 - 1 - orientation) * 4)) >> (4 - 1 - direction)) & 1 def set_transition(self, cell_transition, orientation, direction, new_transition): @@ -276,12 +273,9 @@ class Grid4Transitions(Transitions): """ if new_transition: - cell_transition |= (1 << ((4-1-orientation) * 4 + - (4-1-direction))) + cell_transition |= (1 << ((4 - 1 - orientation) * 4 + (4 - 1 - direction))) else: - cell_transition &= \ - ~(1 << ((4-1-orientation) * 4 + - (4-1-direction))) + cell_transition &= ~(1 << ((4 - 1 - orientation) * 4 + (4 - 1 - direction))) return cell_transition @@ -310,13 +304,11 @@ class Grid4Transitions(Transitions): rotation = rotation // 90 for i in range(4): block_tuple = self.get_transitions(value, i) - block_tuple = block_tuple[( - 4-rotation):] + block_tuple[:(4-rotation)] + block_tuple = block_tuple[(4 - rotation):] + block_tuple[:(4 - rotation)] value = self.set_transitions(value, i, block_tuple) # Rotate the 4-bits blocks - value = ((value & (2**(rotation*4)-1)) << - ((4-rotation)*4)) | (value >> (rotation*4)) + value = ((value & (2**(rotation * 4) - 1)) << ((4 - rotation) * 4)) | (value >> (rotation * 4)) cell_transition = value return cell_transition @@ -355,7 +347,7 @@ class Grid8Transitions(Transitions): List of the validity of transitions in the cell. """ - bits = (cell_transition >> ((7-orientation)*8)) + bits = (cell_transition >> ((7 - orientation) * 8)) cell_transition = ( (bits >> 7) & 1, (bits >> 6) & 1, @@ -389,7 +381,7 @@ class Grid8Transitions(Transitions): `orientation'. """ - mask = (1 << ((8-orientation)*8)) - (1 << ((7-orientation)*8)) + mask = (1 << ((8 - orientation) * 8)) - (1 << ((7 - orientation) * 8)) negmask = ~mask new_transitions = \ @@ -402,8 +394,7 @@ class Grid8Transitions(Transitions): (new_transitions[6] & 1) << 1 | \ (new_transitions[7] & 1) - cell_transition = (cell_transition & negmask) | ( - new_transitions << ((7-orientation)*8)) + cell_transition = (cell_transition & negmask) | (new_transitions << ((7 - orientation) * 8)) return cell_transition @@ -429,8 +420,7 @@ class Grid8Transitions(Transitions): Validity of the requested transition: 0/1 allowed/not allowed. """ - return ((cell_transition >> ((8-1-orientation) * 8)) >> - (8-1-direction)) & 1 + return ((cell_transition >> ((8 - 1 - orientation) * 8)) >> (8 - 1 - direction)) & 1 def set_transition(self, cell_transition, orientation, direction, new_transition): @@ -460,11 +450,9 @@ class Grid8Transitions(Transitions): """ if new_transition: - cell_transition |= (1 << ((8-1-orientation) * 8 + - (8 - 1 - direction))) + cell_transition |= (1 << ((8 - 1 - orientation) * 8 + (8 - 1 - direction))) else: - cell_transition &= ~(1 << ((8-1-orientation) * 8 + - (8 - 1 - direction))) + cell_transition &= ~(1 << ((8 - 1 - orientation) * 8 + (8 - 1 - direction))) return cell_transition @@ -500,8 +488,7 @@ class Grid8Transitions(Transitions): value = self.set_transitions(value, i, block_tuple) # Rotate the 8bits blocks - value = ((value & (2**(rotation*8)-1)) << - ((8-rotation)*8)) | (value >> (rotation*8)) + value = ((value & (2**(rotation * 8) - 1)) << ((8 - rotation) * 8)) | (value >> (rotation * 8)) cell_transition = value diff --git a/flatland/envs/rail_env.py b/flatland/envs/rail_env.py index 9fd85855b094b07b2c2f643c3e39e67de6ff6a32..6750b6a8b0c4854762066dda5fedd8872683e1ac 100644 --- a/flatland/envs/rail_env.py +++ b/flatland/envs/rail_env.py @@ -45,8 +45,7 @@ def rail_from_manual_specifications_generator(rail_spec): if cell[0] < 0 or cell[0] >= len(t_utils.transitions): print("ERROR - invalid cell type=", cell[0]) return [] - rail.set_transitions((r, c), t_utils.rotate_transition( - t_utils.transitions[cell[0]], cell[1])) + rail.set_transitions((r, c), t_utils.rotate_transition(t_utils.transitions[cell[0]], cell[1])) return rail @@ -110,7 +109,7 @@ def generate_rail_from_list_of_manual_specifications(list_of_specifications) """ -def random_rail_generator(cell_type_relative_proportion=[1.0]*8): +def random_rail_generator(cell_type_relative_proportion=[1.0] * 8): """ Dummy random level generator: - fill in cells at random in [width-2, height-2] @@ -149,7 +148,7 @@ def random_rail_generator(cell_type_relative_proportion=[1.0]*8): transitions_templates_ = [] transition_probabilities = [] - for i in range(len(t_utils.transitions)-1): # don't include dead-ends + for i in range(len(t_utils.transitions) - 1): # don't include dead-ends all_transitions = 0 for dir_ in range(4): trans = t_utils.get_transitions(t_utils.transitions[i], dir_) @@ -159,7 +158,7 @@ def random_rail_generator(cell_type_relative_proportion=[1.0]*8): (trans[3]) template = [int(x) for x in bin(all_transitions)[2:]] - template = [0]*(4-len(template)) + template + template = [0] * (4 - len(template)) + template # add all rotations for rot in [0, 90, 180, 270]: @@ -168,7 +167,7 @@ def random_rail_generator(cell_type_relative_proportion=[1.0]*8): t_utils.transitions[i], rot))) transition_probabilities.append(transition_probability[i]) - template = [template[-1]]+template[:-1] + template = [template[-1]] + template[:-1] def get_matching_templates(template): ret = [] @@ -183,7 +182,7 @@ def random_rail_generator(cell_type_relative_proportion=[1.0]*8): ret.append((transitions_templates_[i][1], transition_probabilities[i])) return ret - MAX_INSERTIONS = (width-2) * (height-2) * 10 + MAX_INSERTIONS = (width - 2) * (height - 2) * 10 MAX_ATTEMPTS_FROM_SCRATCH = 10 attempt_number = 0 @@ -191,10 +190,9 @@ def random_rail_generator(cell_type_relative_proportion=[1.0]*8): cells_to_fill = [] rail = [] for r in range(height): - rail.append([None]*width) - if r > 0 and r < height-1: - cells_to_fill = cells_to_fill \ - + [(r, c) for c in range(1, width-1)] + rail.append([None] * width) + if r > 0 and r < height - 1: + cells_to_fill = cells_to_fill + [(r, c) for c in range(1, width - 1)] num_insertions = 0 while num_insertions < MAX_INSERTIONS and len(cells_to_fill) > 0: @@ -212,14 +210,13 @@ def random_rail_generator(cell_type_relative_proportion=[1.0]*8): (1, 3, (0, 1)), (2, 0, (1, 0)), (3, 1, (0, -1))]: # N, E, S, W - neigh_trans = rail[row+el[2][0]][col+el[2][1]] + neigh_trans = rail[row + el[2][0]][col + el[2][1]] if neigh_trans is not None: # select transition coming from facing direction el[1] and # moving to direction el[1] max_bit = 0 for k in range(4): - max_bit |= \ - t_utils.get_transition(neigh_trans, k, el[1]) + max_bit |= t_utils.get_transition(neigh_trans, k, el[1]) if max_bit: valid_template[el[0]] = 1 @@ -244,8 +241,7 @@ def random_rail_generator(cell_type_relative_proportion=[1.0]*8): elif k == 3: rot = 90 - rail[row][col] = t_utils.rotate_transition( - int('0010000000000000', 2), rot) + rail[row][col] = t_utils.rotate_transition(int('0010000000000000', 2), rot) num_insertions += 1 break @@ -258,8 +254,7 @@ def random_rail_generator(cell_type_relative_proportion=[1.0]*8): for k in range(4): tmp_template = valid_template[:] tmp_template[k] = -1 - possible_cell_transitions = get_matching_templates( - tmp_template) + possible_cell_transitions = get_matching_templates(tmp_template) if len(possible_cell_transitions) > len(besttrans): besttrans = possible_cell_transitions bestk = k @@ -284,7 +279,7 @@ def random_rail_generator(cell_type_relative_proportion=[1.0]*8): rail[replace_row][replace_col] = None possible_transitions, possible_probabilities = zip(*besttrans) - possible_probabilities = [p/sum(possible_probabilities) for p in possible_probabilities] + possible_probabilities = [p / sum(possible_probabilities) for p in possible_probabilities] rail[row][col] = np.random.choice(possible_transitions, p=possible_probabilities) @@ -298,7 +293,7 @@ def random_rail_generator(cell_type_relative_proportion=[1.0]*8): else: possible_transitions, possible_probabilities = zip(*possible_cell_transitions) - possible_probabilities = [p/sum(possible_probabilities) for p in possible_probabilities] + possible_probabilities = [p / sum(possible_probabilities) for p in possible_probabilities] rail[row][col] = np.random.choice(possible_transitions, p=possible_probabilities) @@ -321,12 +316,10 @@ def random_rail_generator(cell_type_relative_proportion=[1.0]*8): neigh_trans = rail[r][1] if neigh_trans is not None: for k in range(4): - neigh_trans_from_direction = (neigh_trans >> ((3-k) * 4)) \ - & (2**4-1) + neigh_trans_from_direction = (neigh_trans >> ((3 - k) * 4)) & (2**4 - 1) max_bit = max_bit | (neigh_trans_from_direction & 1) if max_bit: - rail[r][0] = t_utils.rotate_transition( - int('0010000000000000', 2), 270) + rail[r][0] = t_utils.rotate_transition(int('0010000000000000', 2), 270) else: rail[r][0] = int('0000000000000000', 2) @@ -335,8 +328,7 @@ def random_rail_generator(cell_type_relative_proportion=[1.0]*8): neigh_trans = rail[r][-2] if neigh_trans is not None: for k in range(4): - neigh_trans_from_direction = (neigh_trans >> ((3-k) * 4)) \ - & (2**4-1) + neigh_trans_from_direction = (neigh_trans >> ((3 - k) * 4)) & (2**4 - 1) max_bit = max_bit | (neigh_trans_from_direction & (1 << 2)) if max_bit: rail[r][-1] = t_utils.rotate_transition(int('0010000000000000', 2), @@ -350,8 +342,7 @@ def random_rail_generator(cell_type_relative_proportion=[1.0]*8): neigh_trans = rail[1][c] if neigh_trans is not None: for k in range(4): - neigh_trans_from_direction = (neigh_trans >> ((3-k) * 4)) \ - & (2**4-1) + neigh_trans_from_direction = (neigh_trans >> ((3 - k) * 4)) & (2**4 - 1) max_bit = max_bit | (neigh_trans_from_direction & (1 << 3)) if max_bit: rail[0][c] = int('0010000000000000', 2) @@ -363,12 +354,10 @@ def random_rail_generator(cell_type_relative_proportion=[1.0]*8): neigh_trans = rail[-2][c] if neigh_trans is not None: for k in range(4): - neigh_trans_from_direction = (neigh_trans >> ((3-k) * 4)) \ - & (2**4-1) + neigh_trans_from_direction = (neigh_trans >> ((3 - k) * 4)) & (2**4 - 1) max_bit = max_bit | (neigh_trans_from_direction & (1 << 1)) if max_bit: - rail[-1][c] = t_utils.rotate_transition( - int('0010000000000000', 2), 180) + rail[-1][c] = t_utils.rotate_transition(int('0010000000000000', 2), 180) else: rail[-1][c] = int('0000000000000000', 2) @@ -458,8 +447,8 @@ class RailEnv(Environment): self.obs_builder = obs_builder_object self.obs_builder._set_env(self) - self.actions = [0]*self.number_of_agents - self.rewards = [0]*self.number_of_agents + self.actions = [0] * self.number_of_agents + self.rewards = [0] * self.number_of_agents self.done = False self.dones = {"__all__": False} @@ -507,14 +496,13 @@ class RailEnv(Environment): # agents_direction must be a direction for which a solution is # guaranteed. - self.agents_direction = [0]*self.number_of_agents + self.agents_direction = [0] * self.number_of_agents re_generate = False for i in range(self.number_of_agents): valid_movements = [] for direction in range(4): position = self.agents_position[i] - moves = self.rail.get_transitions( - (position[0], position[1], direction)) + moves = self.rail.get_transitions((position[0], position[1], direction)) for move_index in range(4): if moves[move_index]: valid_movements.append((direction, move_index)) @@ -608,8 +596,8 @@ class RailEnv(Environment): reverse_direction = 1 valid_transition = self.rail.get_transition( - (pos[0], pos[1], direction), - reverse_direction) + (pos[0], pos[1], direction), + reverse_direction) if valid_transition: direction = reverse_direction movement = reverse_direction @@ -629,8 +617,8 @@ class RailEnv(Environment): new_cell_isValid = False transition_isValid = self.rail.get_transition( - (pos[0], pos[1], direction), - movement) or is_deadend + (pos[0], pos[1], direction), + movement) or is_deadend cell_isFree = True for j in range(self.number_of_agents): @@ -664,20 +652,20 @@ class RailEnv(Environment): if num_agents_in_target_position == self.number_of_agents: self.dones["__all__"] = True - self.rewards_dict = [r+global_reward for r in self.rewards_dict] + self.rewards_dict = [r + global_reward for r in self.rewards_dict] # Reset the step actions (in case some agent doesn't 'register_action' # on the next step) - self.actions = [0]*self.number_of_agents + self.actions = [0] * self.number_of_agents return self._get_observations(), self.rewards_dict, self.dones, {} def _new_position(self, position, movement): if movement == 0: # NORTH - return (position[0]-1, position[1]) + return (position[0] - 1, position[1]) elif movement == 1: # EAST return (position[0], position[1] + 1) elif movement == 2: # SOUTH - return (position[0]+1, position[1]) + return (position[0] + 1, position[1]) elif movement == 3: # WEST return (position[0], position[1] - 1) diff --git a/flatland/utils/graphics_layer.py b/flatland/utils/graphics_layer.py index b199a3b2e4d71261a402254ccb60ec453e30469c..aa9257b1592b473c5ff1e84d6932805997fab346 100644 --- a/flatland/utils/graphics_layer.py +++ b/flatland/utils/graphics_layer.py @@ -18,16 +18,18 @@ class GraphicsLayer(object): def show(self, block=False): pass - + def pause(self, seconds=0.00001): pass def clf(self): pass - + def beginFrame(self): pass - + def endFrame(self): pass + def getImage(self): + pass diff --git a/flatland/utils/graphics_qt.py b/flatland/utils/graphics_qt.py index 6571f4d7ab03aced3e7846c73ae8439c36e6af42..09dc1fa4d7ea50629a4e1f9f91bb79a549b62ac8 100644 --- a/flatland/utils/graphics_qt.py +++ b/flatland/utils/graphics_qt.py @@ -123,7 +123,7 @@ class QtRenderer(object): def beginFrame(self): self.painter.begin(self.img) - self.painter.setRenderHint(QPainter.Antialiasing, False) + # self.painter.setRenderHint(QPainter.Antialiasing, False) # Clear the background self.painter.setBrush(QColor(0, 0, 0)) @@ -214,13 +214,12 @@ class QtRenderer(object): def takeSnapshot(self, sDir="./movie"): oWidget = self.window.mainWidget oPixmap = oWidget.grab() - + if not os.path.isdir(sDir): os.mkdir(sDir) - + nRunIn = 30 if self.iFrame > nRunIn: sfImage = "%s/frame%05d.jpg" % (sDir, self.iFrame - nRunIn) oPixmap.save(sfImage, "jpg") self.iFrame += 1 - diff --git a/flatland/utils/render_qt.py b/flatland/utils/render_qt.py index 60b8d2952d23289cafe7648f4adc1ab5527bac5f..34e198566fdd8df8cff7e1822934e1f04cf3945c 100644 --- a/flatland/utils/render_qt.py +++ b/flatland/utils/render_qt.py @@ -2,6 +2,7 @@ from flatland.utils.graphics_qt import QtRenderer from numpy import array from flatland.utils.graphics_layer import GraphicsLayer from matplotlib import pyplot as plt +import numpy as np class QTGL(GraphicsLayer): @@ -35,8 +36,7 @@ class QTGL(GraphicsLayer): self.qtr.pop() self.qtr.endFrame() - def plot(self, gX, gY, color=None, linewidth=2, **kwargs): - + def adaptColor(self, color): if color == "red" or color == "r": color = (255, 0, 0) elif color == "gray": @@ -48,36 +48,52 @@ class QTGL(GraphicsLayer): color = gcolor[:3] * 255 else: color = self.tColGrid + return color + + def plot(self, gX, gY, color=None, linewidth=2, **kwargs): + color = self.adaptColor(color) self.qtr.setLineColor(*color) lastx = lasty = None - for x, y in zip(gX, gY): - if lastx is not None: - # print("line", lastx, lasty, x, y) - self.qtr.drawLine( - lastx*self.cell_pixels, -lasty*self.cell_pixels, - x*self.cell_pixels, -y*self.cell_pixels) - lastx = x - lasty = y - - def scatter(self, *args, **kwargs): - print("scatter not yet implemented in ", self.__class__) + + if False: + for x, y in zip(gX, gY): + if lastx is not None: + # print("line", lastx, lasty, x, y) + self.qtr.drawLine( + lastx*self.cell_pixels, -lasty*self.cell_pixels, + x*self.cell_pixels, -y*self.cell_pixels) + lastx = x + lasty = y + else: + # print(gX, gY) + gPoints = np.stack([array(gX), -array(gY)]).T * self.cell_pixels + self.qtr.drawPolyline(gPoints) + + def scatter(self, gX, gY, color=None, marker="o", size=5, *args, **kwargs): + color = self.adaptColor(color) + self.qtr.setColor(*color) + self.qtr.setLineColor(*color) + r = np.sqrt(size) + gPoints = np.stack([np.atleast_1d(gX), -np.atleast_1d(gY)]).T * self.cell_pixels + for x, y in gPoints: + self.qtr.drawCircle(x, y, r) def text(self, x, y, sText): - self.qtr.drawText(x*self.cell_pixels, -y*self.cell_pixels, sText) - + self.qtr.drawText(x * self.cell_pixels, -y * self.cell_pixels, sText) + def prettify(self, *args, **kwargs): pass def prettify2(self, width, height, cell_size): pass - + def show(self, block=False): pass def pause(self, seconds=0.00001): pass - + def clf(self): pass @@ -88,9 +104,7 @@ class QTGL(GraphicsLayer): self.qtr.beginFrame() self.qtr.push() self.qtr.fillRect(0, 0, self.widthPx, self.heightPx, *self.tColBg) - + def endFrame(self): self.qtr.pop() self.qtr.endFrame() - - diff --git a/flatland/utils/rendertools.py b/flatland/utils/rendertools.py index f9ebf5328556d40fbe739557c3ec7b73907bf90d..edf52926ca6658d2ac92de17c0e16b209947664b 100644 --- a/flatland/utils/rendertools.py +++ b/flatland/utils/rendertools.py @@ -9,6 +9,7 @@ from collections import deque from flatland.utils.render_qt import QTGL from flatland.utils.graphics_layer import GraphicsLayer + # TODO: suggested renaming to RailEnvRenderTool, as it will only work with RailEnv! @@ -24,11 +25,11 @@ class MPLGL(GraphicsLayer): def text(self, *args, **kwargs): plt.text(*args, **kwargs) - + def prettify(self, *args, **kwargs): ax = plt.gca() - plt.xticks(range(int(ax.get_xlim()[1])+1)) - plt.yticks(range(int(ax.get_ylim()[1])+1)) + plt.xticks(range(int(ax.get_xlim()[1]) + 1)) + plt.yticks(range(int(ax.get_ylim()[1]) + 1)) plt.grid() plt.xlabel("Euclidean distance") plt.ylabel("Tree / Transition Depth") @@ -41,31 +42,40 @@ class MPLGL(GraphicsLayer): gLabels = np.arange(0, height) plt.xticks(gTicks, gLabels) - gTicks = np.arange(-height * cell_size, 0) + cell_size/2 - gLabels = np.arange(height-1, -1, -1) + gTicks = np.arange(-height * cell_size, 0) + cell_size / 2 + gLabels = np.arange(height - 1, -1, -1) plt.yticks(gTicks, gLabels) plt.xlim([0, width * cell_size]) plt.ylim([-height * cell_size, 0]) - + def show(self, block=False): plt.show(block=block) def pause(self, seconds=0.00001): plt.pause(seconds) - + def clf(self): plt.clf() - + def get_cmap(self, *args, **kwargs): return plt.get_cmap(*args, **kwargs) def beginFrame(self): pass - + def endFrame(self): pass + def getImage(self): + ax = plt.gca() + fig = ax.get_figure() + fig.tight_layout(pad=0) + fig.canvas.draw() + data = np.frombuffer(fig.canvas.tostring_rgb(), dtype=np.uint8) + data = data.reshape(fig.canvas.get_width_height()[::-1] + (3,)) + return data + class RenderTool(object): Visit = recordtype("Visit", ["rc", "iDir", "iDepth", "prev"]) @@ -85,7 +95,7 @@ class RenderTool(object): gCentres = xr.DataArray(gGrid, dims=["xy", "p1", "p2"], coords={"xy": ["x", "y"]}) + xyPixHalf - gTheta = np.linspace(0, np.pi/2, 10) + gTheta = np.linspace(0, np.pi / 2, 10) gArc = array([np.cos(gTheta), np.sin(gTheta)]).T # from [1,0] to [0,1] def __init__(self, env, gl="MPL"): @@ -116,8 +126,8 @@ class RenderTool(object): self.plotAgent(rcPos, iDir, sColor) - gTransRCAg = self.getTransRC(rcPos, iDir) - self.plotTrans(rcPos, gTransRCAg, color=color) + # gTransRCAg = self.getTransRC(rcPos, iDir) + # self.plotTrans(rcPos, gTransRCAg, color=color) if False: # TODO: this was `rcDir' but it was undefined @@ -134,21 +144,22 @@ class RenderTool(object): gTransRCAg = rt.gTransRC[giTrans] self.plotTrans(visit.rc, gTransRCAg, depth=str(visit.iDepth), color=color) - def plotAgents(self): - rt = self.__class__ - - # plt.scatter(*rt.gCentres, s=5, color="r") - + def plotAgents(self, targets=True): + cmap = self.gl.get_cmap('hsv', lut=self.env.number_of_agents+1) for iAgent in range(self.env.number_of_agents): - sColor = rt.lColors[iAgent] + oColor = cmap(iAgent) rcPos = self.env.agents_position[iAgent] iDir = self.env.agents_direction[iAgent] # agent direction index - self.plotAgent(rcPos, iDir, sColor) + if targets: + target = self.env.agents_target[iAgent] + else: + target = None + self.plotAgent(rcPos, iDir, oColor, target=target) - gTransRCAg = self.getTransRC(rcPos, iDir) - self.plotTrans(rcPos, gTransRCAg) + # gTransRCAg = self.getTransRC(rcPos, iDir) + # self.plotTrans(rcPos, gTransRCAg) def getTransRC(self, rcPos, iDir, bgiTrans=False): """ @@ -186,24 +197,32 @@ class RenderTool(object): else: return gTransRCAg - def plotAgent(self, rcPos, iDir, sColor="r"): + def plotAgent(self, rcPos, iDir, color="r", target=None): """ Plot a simple agent. - Assumes a working matplotlib context. + Assumes a working graphics layer context (cf a MPL figure). """ rt = self.__class__ - xyPos = np.matmul(rcPos, rt.grc2xy) + rt.xyHalf - self.gl.scatter(*xyPos, color=sColor) # agent location rcDir = rt.gTransRC[iDir] # agent direction in RC xyDir = np.matmul(rcDir, rt.grc2xy) # agent direction in xy - xyDirLine = array([xyPos, xyPos+xyDir/2]).T # line for agent orient. - self.gl.plot(*xyDirLine, color=sColor, lw=5, ms=0, alpha=0.6) - # just mark the next cell we're heading into - rcNext = rcPos + rcDir - xyNext = np.matmul(rcNext, rt.grc2xy) + rt.xyHalf - self.gl.scatter(*xyNext, color=sColor) + xyPos = np.matmul(rcPos - rcDir / 2, rt.grc2xy) + rt.xyHalf + self.gl.scatter(*xyPos, color=color, size=40) # agent location + + xyDirLine = array([xyPos, xyPos + xyDir/2]).T # line for agent orient. + self.gl.plot(*xyDirLine, color=color, lw=5, ms=0, alpha=0.6) + + if target is not None: + rcTarget = array(target) + xyTarget = np.matmul(rcTarget, rt.grc2xy) + rt.xyHalf + self._draw_square(xyTarget, 1/3, color) + + if False: + # mark the next cell we're heading into + rcNext = rcPos + rcDir + xyNext = np.matmul(rcNext, rt.grc2xy) + rt.xyHalf + self.gl.scatter(*xyNext, color=color) def plotTrans(self, rcPos, gTransRCAg, color="r", depth=None): """ @@ -215,7 +234,7 @@ class RenderTool(object): rt = self.__class__ xyPos = np.matmul(rcPos, rt.grc2xy) + rt.xyHalf - gxyTrans = xyPos + np.matmul(gTransRCAg, rt.grc2xy/2.4) + gxyTrans = xyPos + np.matmul(gTransRCAg, rt.grc2xy / 2.4) self.gl.scatter(*gxyTrans.T, color=color, marker="o", s=50, alpha=0.2) if depth is not None: for x, y in gxyTrans: @@ -255,7 +274,7 @@ class RenderTool(object): # print("Trans:", gTransRC2) visitNext = rt.Visit(tuple(visit.rc + gTransRC2), iTrans, - visit.iDepth+1, + visit.iDepth + 1, visit) # print("node2: ", node2) stack.append(visitNext) @@ -294,7 +313,7 @@ class RenderTool(object): xLoc = rDist + visit.iDir / 4 # point labelled with distance - self.gl.scatter(xLoc, visit.iDepth, color="k", s=2) + self.gl.scatter(xLoc, visit.iDepth, color="k", s=2) # plt.text(xLoc, visit.iDepth, sDist, color="k", rotation=45) self.gl.text(xLoc, visit.iDepth, visit.rc, color="k", rotation=45) @@ -312,8 +331,8 @@ class RenderTool(object): # line from prev node self.gl.plot([xLocPrev, xLoc], - [visit.iDepth-1, visit.iDepth], - color="k", alpha=0.5, lw=1) + [visit.iDepth - 1, visit.iDepth], + color="k", alpha=0.5, lw=1) if rDist < 0.1: visitDest = visit @@ -326,8 +345,8 @@ class RenderTool(object): rDist = np.linalg.norm(array(visit.rc) - array(xyTarg)) xLoc = rDist + visit.iDir / 4 if xLocPrev is not None: - self.gl.plot([xLoc, xLocPrev], [visit.iDepth, visit.iDepth+1], - color="r", alpha=0.5, lw=2) + self.gl.plot([xLoc, xLocPrev], [visit.iDepth, visit.iDepth + 1], + color="r", alpha=0.5, lw=2) xLocPrev = xLoc visit = visit.prev # prev = prev.prev @@ -360,13 +379,12 @@ class RenderTool(object): self.gl.plot(*xyLine.T, color="r", alpha=0.5, lw=1) - xyMid = np.sum(xyLine * [[1/4], [3/4]], axis=0) + xyMid = np.sum(xyLine * [[1 / 4], [3 / 4]], axis=0) xyArrow = array([ - xyMid + [-dx-dy, +dx-dy], + xyMid + [-dx - dy, +dx - dy], xyMid, - xyMid + [-dx+dy, -dx-dy] - ]) + xyMid + [-dx + dy, -dx - dy]]) self.gl.plot(*xyArrow.T, color="r") visit = visit.prev @@ -411,13 +429,12 @@ class RenderTool(object): self.gl.plot(*xyLine2.T, color=sColor) if bArrow: - xyMid = np.sum(xyLine2 * [[1/4], [3/4]], axis=0) + xyMid = np.sum(xyLine2 * [[1 / 4], [3 / 4]], axis=0) xyArrow = array([ - xyMid + [-dx-dy, +dx-dy], + xyMid + [-dx - dy, +dx - dy], xyMid, - xyMid + [-dx+dy, -dx-dy] - ]) + xyMid + [-dx + dy, -dx - dy]]) self.gl.plot(*xyArrow.T, color=sColor) else: @@ -443,10 +460,9 @@ class RenderTool(object): iArc = int(len(rt.gArc) / 2) xyMid = xyCorner + rt.gArc[iArc] * dxy2 xyArrow = array([ - xyMid + [-dx-dy, +dx-dy], + xyMid + [-dx - dy, +dx - dy], xyMid, - xyMid + [-dx+dy, -dx-dy] - ]) + xyMid + [-dx + dy, -dx - dy]]) self.gl.plot(*xyArrow.T, color=sColor) def renderEnv( @@ -480,14 +496,14 @@ class RenderTool(object): # Draw cells grid grid_color = [0.95, 0.95, 0.95] - for r in range(env.height+1): - self.gl.plot([0, (env.width+1)*cell_size], - [-r*cell_size, -r*cell_size], - color=grid_color) - for c in range(env.width+1): - self.gl.plot([c*cell_size, c*cell_size], - [0, -(env.height+1)*cell_size], - color=grid_color) + for r in range(env.height + 1): + self.gl.plot([0, (env.width + 1) * cell_size], + [-r * cell_size, -r * cell_size], + color=grid_color) + for c in range(env.width + 1): + self.gl.plot([c * cell_size, c * cell_size], + [0, -(env.height + 1) * cell_size], + color=grid_color) # Draw each cell independently for r in range(env.height): @@ -495,16 +511,16 @@ class RenderTool(object): # bounding box of the grid cell x0 = cell_size * c # left - x1 = cell_size * (c+1) # right + x1 = cell_size * (c + 1) # right y0 = cell_size * -r # top - y1 = cell_size * -(r+1) # bottom + y1 = cell_size * -(r + 1) # bottom # centres of cell edges coords = [ - ((x0+x1)/2.0, y0), # N middle top - (x1, (y0+y1)/2.0), # E middle right - ((x0+x1)/2.0, y1), # S middle bottom - (x0, (y0+y1)/2.0) # W middle left + ((x0 + x1) / 2.0, y0), # N middle top + (x1, (y0 + y1) / 2.0), # E middle right + ((x0 + x1) / 2.0, y1), # S middle bottom + (x0, (y0 + y1) / 2.0) # W middle left ] # cell centre @@ -571,20 +587,23 @@ class RenderTool(object): # Draw each agent + its orientation + its target if agents: cmap = self.gl.get_cmap('hsv', lut=env.number_of_agents+1) + self.plotAgents(targets=True) + + if False: for i in range(env.number_of_agents): self._draw_square(( - env.agents_position[i][1] * - cell_size+cell_size/2, - -env.agents_position[i][0] * - cell_size-cell_size/2), - cell_size/8, cmap(i)) + env.agents_position[i][1] * + cell_size + cell_size / 2, + -env.agents_position[i][0] * + cell_size - cell_size / 2), + cell_size / 8, cmap(i)) for i in range(env.number_of_agents): self._draw_square(( - env.agents_target[i][1] * - cell_size+cell_size/2, - -env.agents_target[i][0] * - cell_size-cell_size/2), - cell_size/3, [c for c in cmap(i)]) + env.agents_target[i][1] * + cell_size + cell_size / 2, + -env.agents_target[i][0] * + cell_size - cell_size / 2), + cell_size / 3, [c for c in cmap(i)]) # orientation is a line connecting the center of the cell to the # side of the square of the agent @@ -594,8 +613,8 @@ class RenderTool(object): (new_position[1] + env.agents_position[i][1]) / 2 * cell_size) self.gl.plot( - [env.agents_position[i][1] * cell_size+cell_size/2, new_position[1]+cell_size/2], - [-env.agents_position[i][0] * cell_size-cell_size/2, -new_position[0]-cell_size/2], + [env.agents_position[i][1] * cell_size + cell_size / 2, new_position[1] + cell_size / 2], + [-env.agents_position[i][0] * cell_size - cell_size / 2, -new_position[0] - cell_size / 2], color=cmap(i), linewidth=2.0) @@ -604,7 +623,7 @@ class RenderTool(object): if frames: self.gl.text(0.1, yText[2], "Frame:{:}".format(self.iFrame)) self.iFrame += 1 - + if iEpisode is not None: self.gl.text(0.1, yText[1], "Ep:{}".format(iEpisode)) @@ -631,8 +650,12 @@ class RenderTool(object): return def _draw_square(self, center, size, color): - x0 = center[0]-size/2 - x1 = center[0]+size/2 - y0 = center[1]-size/2 - y1 = center[1]+size/2 + x0 = center[0] - size / 2 + x1 = center[0] + size / 2 + y0 = center[1] - size / 2 + y1 = center[1] + size / 2 self.gl.plot([x0, x1, x1, x0, x0], [y0, y0, y1, y1, y0], color=color) + + def getImage(self): + return self.gl.getImage() + diff --git a/notebooks/CanvasEditor.ipynb b/notebooks/CanvasEditor.ipynb new file mode 100644 index 0000000000000000000000000000000000000000..7b96f4535bf695ed669022ab900eb5bb66f98d73 --- /dev/null +++ b/notebooks/CanvasEditor.ipynb @@ -0,0 +1,1149 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Jupyter Canvas Widget - Rail Editor\n", + "\n", + "From - https://github.com/Who8MyLunch/Jupyter_Canvas_Widget/blob/master/notebooks/example%20mouse%20events.ipynb\n", + "Follow his instructions to do a local dev install and enable the widget." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## You need to run all cells before trying to edit the rails!" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "%load_ext autoreload\n", + "%autoreload 2" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "import image_attendant as imat\n", + "import ipywidgets\n", + "import IPython\n", + "import jpy_canvas\n", + "import numpy as np\n", + "from numpy import array\n", + "import time\n", + "from collections import deque\n", + "from matplotlib import pyplot as plt\n", + "import io\n", + "from PIL import Image" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "from ipywidgets import IntSlider, link, VBox" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "import flatland.core.env\n", + "from flatland.envs.rail_env import RailEnv, random_rail_generator\n", + "from flatland.core.transitions import RailEnvTransitions\n", + "from flatland.core.env_observation_builder import TreeObsForRailEnv\n", + "import flatland.utils.rendertools as rt" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "<style>.container { width:90% !important; }</style>" + ], + "text/plain": [ + "<IPython.core.display.HTML object>" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from IPython.core.display import display, HTML\n", + "display(HTML(\"<style>.container { width:90% !important; }</style>\"))" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "oEnv = RailEnv(width=10,\n", + " height=10,\n", + " rail_generator=random_rail_generator(cell_type_relative_proportion=[1,1] + [0.5] * 6),\n", + " number_of_agents=0,\n", + " obs_builder_object=TreeObsForRailEnv(max_depth=2))\n", + "obs = oEnv.reset()\n", + "\n", + "oRT = rt.RenderTool(oEnv)" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "sfEnv = \"../flatland/env-data/tests/test1.npy\"\n", + "oEnv.rail.load_transition_map(sfEnv)" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAt4AAALeCAYAAACdlBZfAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADl0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uIDMuMC4yLCBodHRwOi8vbWF0cGxvdGxpYi5vcmcvOIA7rQAAIABJREFUeJzs3XdwVOeh///32RUqiwrqFNMkC5CQkMDCxsbBBQOm2biCMSCnOc0luXFmPL+Z5HuTuZnrm3iSXDu5cYtNM8a4G0QvNj0U0SyKEUKWAAFCqCCEymrP7w+sNUXYArR7jlaf14xn5NXR7oeHZfXZZ5/zHMM0TURERERExLccVgcQEREREekIVLxFRERERPxAxVtERERExA9UvEVERERE/EDFW0RERETED1S8RURERET8QMVbRERERMQPVLxFRERERPxAxVtERERExA+CfHGnsbGxZu/evX1x1yIiIiIitrJjx45TpmnGf9dxPinevXv3Ji8vzxd33W7V1tYC4HK5LE5iLxqXK9PYtEzj0jKNS8s0LlemsWmZxqVlGpcrq62tpXPnzl+15lgtNRERERER8QMVbxERERERP1DxFhERERHxAxVvERERERE/UPEWEREREfEDFW8RERERET9Q8RYRERER8QMVbxERERERP1DxFhERERHxAxVvERERERE/UPEWEREREfEDFW8RERERET9Q8RYRERER8QMVbxERERERP1DxFhERERHxAxVvERERERE/UPEWEREREfEDFW8RERERET9Q8RYRERER8QMVbxERERERP1DxFhERERHxAxVvERERERE/UPEWEREREfEDFW8RERERET9Q8RYRERER8QMVbxERERERP1DxFhERERHxAxVvERERERE/UPEWEREREfEDFW8RERERET9oVfE2DONewzAOGIZRYBjG874OJSIiIiISaL6zeBuG4QT+AYwF0oDHDMNI83UwEREREZFA0poZ75uBAtM0C03TbADmA/f7Npb40pw5c5gzZ47VMUTaPf1bkqul54xcDT1fAk9QK47pAZRc8P9HgFsuPcgwjCeBJwHS09PbJJz4RmFhodURRAKC/i3J1dJzRq6Gni+Bp81OrjRN8zXTNLNN08zu1KlTW92tiIiIiEhAaE3xPgr0vOD/b/j6NhERERERaaXWFO+tQIph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\n", + "text/plain": [ + "<Figure size 720x720 with 1 Axes>" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "oFig = plt.figure(figsize=(10,10))\n", + "oRT.renderEnv(spacing=False, arrows=False, sRailColor=\"gray\", show=False)\n", + "img = oRT.getImage()\n", + "#plt.clf()\n", + "pass" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "# This API call is misleading - it doesn't update the env's transition map.\n", + "oEnv.rail.set_transition((1,1,2), 1, True)" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "0" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "oEnv.rail.get_transition((1,1,2), 1)" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'0b1000000000100000'" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "bin(oEnv.rail.grid[1,1])" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'0b1000000001100000'" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "cell_id = (1,1,2)\n", + "iDir = 1\n", + "iValCell = oEnv.rail.transitions.set_transition(oEnv.rail.grid[cell_id[0]][cell_id[1]], cell_id[2], iDir, True)\n", + "bin(iValCell)" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [], + "source": [ + "oEnv.rail.grid[cell_id[0]][cell_id[1]] = iValCell" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'0b1000000001100000'" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "bin(oEnv.rail.grid[1,1])" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [], + "source": [ + "#image = imat.read(\"../Jupyter_Canvas_Widget/notebooks/images/mini_1.jpg\")\n", + "image = img\n", + "image_b = imat.rebin(image, 0.25) \n", + "\n", + "H,W = image.shape[:2]\n", + "\n", + "L = 20\n", + "L2 = L*2 + 1" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [], + "source": [ + "wid_img = jpy_canvas.Canvas(image)\n", + "wid_sub = jpy_canvas.Canvas(image_b)\n", + "wid_sub.width=300\n", + "wid_sub.layout.border='black'\n", + "\n", + "wid_img.width = W \n", + "wid_img.height = H \n", + "\n", + "# wid_sub.width = L2*3\n", + "# wid_sub.height = L2*3\n", + "\n", + "# guessing these:\n", + "xyBase = array([20,20])\n", + "nPixCell = 70\n", + "\n", + "#wid_box = ipywidgets.HBox([wid_img, wid_sub])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Edit the map below here by dragging the mouse to create transitions" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": { + "scrolled": false + }, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "e92617af405d4215ac1f02eed0c456ae", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Canvas()" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[2 2] [0 1]\n", + "[0 1] [1 1]\n", + "iTrans: 2\n", + "[1 1] [1 2]\n", + "iTrans: 1\n", + "iTransLast 2\n", + "Set RCD: [1 1] 2 to: 1\n" + ] + }, + { + "data": { + "text/plain": [ + "<Figure size 720x720 with 0 Axes>" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[1 2] [1 3]\n", + "iTrans: 1\n", + "iTransLast 1\n", + "Set RCD: [1 2] 1 to: 1\n" + ] + }, + { + "data": { + "text/plain": [ + "<Figure size 720x720 with 0 Axes>" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[1 3] [2 3]\n", + "iTrans: 2\n", + "iTransLast 1\n", + "Set RCD: [1 3] 1 to: 2\n" + ] + }, + { + "data": { + "text/plain": [ + "<Figure size 720x720 with 0 Axes>" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[2 3] [2 2]\n", + "iTrans: 3\n", + "iTransLast 2\n", + "Set RCD: [2 3] 2 to: 3\n" + ] + }, + { + "data": { + "text/plain": [ + "<Figure size 720x720 with 0 Axes>" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[2 2] [2 1]\n", + "iTrans: 3\n", + "iTransLast 3\n", + "Set RCD: [2 2] 3 to: 3\n" + ] + }, + { + "data": { + "text/plain": [ + "<Figure size 720x720 with 0 Axes>" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[2 1] [2 2]\n", + "iTrans: 1\n", + "iTransLast 3\n", + "Set RCD: [2 1] 3 to: 1\n" + ] + }, + { + "data": { + "text/plain": [ + "<Figure size 720x720 with 0 Axes>" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[2 2] [2 3]\n", + "iTrans: 1\n", + "iTransLast 1\n", + "Set RCD: [2 2] 1 to: 1\n" + ] + }, + { + "data": { + "text/plain": [ + "<Figure size 720x720 with 0 Axes>" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[2 3] [2 2]\n", + "iTrans: 3\n", + "iTransLast 1\n", + "Set RCD: [2 3] 1 to: 3\n" + ] + }, + { + "data": { + "text/plain": [ + "<Figure size 720x720 with 0 Axes>" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[2 2] [2 1]\n", + "iTrans: 3\n", + "iTransLast 3\n", + "Set RCD: [2 2] 3 to: 3\n" + ] + }, + { + "data": { + "text/plain": [ + "<Figure size 720x720 with 0 Axes>" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[2 1] [2 2]\n", + "iTrans: 1\n", + "iTransLast 3\n", + "Set RCD: [2 1] 3 to: 1\n" + ] + }, + { + "data": { + "text/plain": [ + "<Figure size 720x720 with 0 Axes>" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[2 2] [2 1]\n", + "iTrans: 3\n", + "iTransLast 1\n", + "Set RCD: [2 2] 1 to: 3\n" + ] + }, + { + "data": { + "text/plain": [ + "<Figure size 720x720 with 0 Axes>" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[2 1] [2 2]\n", + "iTrans: 1\n", + "iTransLast 3\n", + "Set RCD: [2 1] 3 to: 1\n" + ] + }, + { + "data": { + "text/plain": [ + "<Figure size 720x720 with 0 Axes>" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[2 2] [2 1]\n", + "iTrans: 3\n", + "iTransLast 1\n", + "Set RCD: [2 2] 1 to: 3\n" + ] + }, + { + "data": { + "text/plain": [ + "<Figure size 720x720 with 0 Axes>" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[2 1] [2 2]\n", + "iTrans: 1\n", + "iTransLast 3\n", + "Set RCD: [2 1] 3 to: 1\n" + ] + }, + { + "data": { + "text/plain": [ + "<Figure size 720x720 with 0 Axes>" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[2 2] [2 1]\n", + "iTrans: 3\n", + "iTransLast 1\n", + "Set RCD: [2 2] 1 to: 3\n" + ] + }, + { + "data": { + "text/plain": [ + "<Figure size 720x720 with 0 Axes>" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[2 1] [2 2]\n", + "iTrans: 1\n", + "iTransLast 3\n", + "Set RCD: [2 1] 3 to: 1\n" + ] + }, + { + "data": { + "text/plain": [ + "<Figure size 720x720 with 0 Axes>" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[2 2] [2 1]\n", + "iTrans: 3\n", + "iTransLast 1\n", + "Set RCD: [2 2] 1 to: 3\n" + ] + }, + { + "data": { + "text/plain": [ + "<Figure size 720x720 with 0 Axes>" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[2 1] [2 2]\n", + "iTrans: 1\n", + "iTransLast 3\n", + "Set RCD: [2 1] 3 to: 1\n" + ] + }, + { + "data": { + "text/plain": [ + "<Figure size 720x720 with 0 Axes>" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[2 2] [2 1]\n", + "iTrans: 3\n", + "iTransLast 1\n", + "Set RCD: [2 2] 1 to: 3\n" + ] + }, + { + "data": { + "text/plain": [ + "<Figure size 720x720 with 0 Axes>" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[2 1] [2 2]\n", + "iTrans: 1\n", + "iTransLast 3\n", + "Set RCD: [2 1] 3 to: 1\n" + ] + }, + { + "data": { + "text/plain": [ + "<Figure size 720x720 with 0 Axes>" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[2 2] [2 1]\n", + "iTrans: 3\n", + "iTransLast 1\n", + "Set RCD: [2 2] 1 to: 3\n" + ] + }, + { + "data": { + "text/plain": [ + "<Figure size 720x720 with 0 Axes>" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[2 1] [2 2]\n", + "iTrans: 1\n", + "iTransLast 3\n", + "Set RCD: [2 1] 3 to: 1\n" + ] + }, + { + "data": { + "text/plain": [ + "<Figure size 720x720 with 0 Axes>" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[2 2] [2 1]\n", + "iTrans: 3\n", + "iTransLast 1\n", + "Set RCD: [2 2] 1 to: 3\n" + ] + }, + { + "data": { + "text/plain": [ + "<Figure size 720x720 with 0 Axes>" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[2 1] [3 1]\n", + "iTrans: 2\n", + "iTransLast 3\n", + "Set RCD: [2 1] 3 to: 2\n" + ] + }, + { + "data": { + "text/plain": [ + "<Figure size 720x720 with 0 Axes>" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[3 1] [1 2]\n", + "[1 2] [1 1]\n", + "iTrans: 3\n", + "iTransLast 2\n", + "Set RCD: [1 2] 2 to: 3\n" + ] + }, + { + "data": { + "text/plain": [ + "<Figure size 720x720 with 0 Axes>" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "#wid_box\n", + "wid_img" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "0" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "lEvDraw = deque()\n", + "\n", + "rcLast = array([-1,-1])\n", + "iTransLast = -1\n", + "\n", + "gRCTrans = array([[-1,0], [0,1], [1,0], [0,-1]]) # NESW in RC\n", + "rcTrans = array([1,1])\n", + "iTrans = np.argwhere(np.all(gRCTrans - rcTrans == 0, axis=1))\n", + "len(iTrans)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [], + "source": [ + "def work_function(wid, event):\n", + " \"\"\"Mouse motion event handler\n", + " \"\"\"\n", + " global rcLast, iTransLast\n", + " \n", + " i = event['canvasX'] \n", + " i0 = i-L\n", + " i1 = i+L+1\n", + "\n", + " j = event['canvasY']\n", + " j0 = j-L\n", + " j1 = j+L+1\n", + "\n", + " if i0 < 0:\n", + " i0 = 0\n", + " \n", + " if j0 < 0:\n", + " j0 = 0\n", + " \n", + " #crop = wid.data[j0:j1, i0:i1]\n", + " #print(event)\n", + " #print(i0,i1,j0,j1)\n", + " #print(wid.data[i,j])\n", + " #print(crop.shape)\n", + " \n", + " if False:\n", + " with wid_sub.hold_sync():\n", + " wid_sub.data = crop\n", + " wid_sub.width = crop.shape[1]*5\n", + " wid_sub.height = crop.shape[0]*5\n", + "\n", + " \n", + " if event[\"buttons\"] > 0:\n", + " if False:\n", + " width, height = wid.data.shape[:2]\n", + " with wid.hold_sync():\n", + "\n", + " if i>10 and i<width and j> 10 and j < height:\n", + " writableData = np.copy(wid.data)\n", + " writableData[j-5:j+5, i-5:i+5, :] = 255\n", + " wid.data = writableData\n", + " else:\n", + " lEvDraw.append((time.time(), i,j))\n", + " \n", + " if len(lEvDraw) > 0:\n", + " tNow = time.time()\n", + " if tNow - lEvDraw[0][0] > 0.1: # wait before trying to draw\n", + " height, width = wid.data.shape[:2]\n", + " writableData = np.copy(wid.data)\n", + " bRedrawn = False\n", + " with wid.hold_sync():\n", + " #rcLast = array([-1,-1])\n", + " while len(lEvDraw) > 0:\n", + " t, i, j = lEvDraw.popleft()\n", + " #print(\"tij:\", t,i,j)\n", + " if i>10 and i<width and j> 10 and j < height:\n", + " writableData[j-2:j+2, i-2:i+2, :] = 0\n", + " \n", + " rcCell = ((array([j,i]) - xyBase) / nPixCell).astype(int)\n", + " \n", + " if (not np.array_equal(rcLast, array([-1,-1]))) and not np.array_equal(rcLast, rcCell):\n", + " print (rcLast, rcCell) \n", + " rcTrans = rcCell - rcLast\n", + " iTrans = np.argwhere(np.all(gRCTrans - rcTrans == 0, axis=1))\n", + " if len(iTrans) > 0:\n", + " iTrans = iTrans[0][0]\n", + " print(\"iTrans: \", iTrans)\n", + " if iTransLast >= 0:\n", + " print(\"iTransLast\", iTransLast)\n", + " print(\"Set RCD:\", rcLast, iTransLast, \"to: \", iTrans )\n", + " #oEnv.rail.set_transition((*rcLast, iTransLast), iTrans, True) # does nothing\n", + " iValCell = oEnv.rail.transitions.set_transition(oEnv.rail.grid[rcLast[0], rcLast[1]], iTransLast, iTrans, True)\n", + " oEnv.rail.grid[rcLast[0], rcLast[1]] = iValCell\n", + " \n", + " oFig = plt.figure(figsize=(10,10))\n", + " oRT.renderEnv(spacing=False, arrows=False, sRailColor=\"gray\", show=False)\n", + " img = oRT.getImage()\n", + " plt.clf()\n", + " wid.data = img\n", + " bRedrawn = True\n", + " \n", + " \n", + " iTransLast = iTrans\n", + " rcLast = rcCell\n", + " \n", + " if not bRedrawn:\n", + " wid.data = writableData\n", + " #wid.width = W \n", + " #wid.height = H" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [], + "source": [ + "wid_img.register_move(work_function)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Junk below here" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array([[[255, 255, 255],\n", + " [255, 255, 255],\n", + " [255, 255, 255]],\n", + "\n", + " [[255, 255, 255],\n", + " [255, 255, 255],\n", + " [255, 255, 255]],\n", + "\n", + " [[255, 255, 255],\n", + " [255, 255, 255],\n", + " [255, 255, 255]]], dtype=uint8)" + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "crop = wid_img.data[0:3, 0:3]\n", + "crop" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "<class 'numpy.ndarray'> (720, 720, 3)\n" + ] + } + ], + "source": [ + "image2 = np.copy(image)\n", + "print(type(image2), image2.shape)" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(720, 720)" + ] + }, + "execution_count": 23, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "W,H\n" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array([2, 3])" + ] + }, + "execution_count": 24, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "array([2,3]).astype(int)" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array([[0., 0., 1., 0., 0.],\n", + " [0., 0., 1., 0., 0.]])" + ] + }, + "execution_count": 25, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "gA = np.zeros((5,5))\n", + "gA[2,2]= 1\n", + "\n", + "rcLast = array([2,2])\n", + "gA[rcLast.T] \n", + "#gA" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "\n" + ] + } + ], + "metadata": { + "hide_input": false, + "kernelspec": { + "display_name": "ve367", + "language": "python", + "name": "ve367" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.7" + }, + "latex_envs": { + "LaTeX_envs_menu_present": true, + "autoclose": false, + "autocomplete": true, + "bibliofile": "biblio.bib", + "cite_by": "apalike", + "current_citInitial": 1, + "eqLabelWithNumbers": true, + "eqNumInitial": 1, + "hotkeys": { + "equation": "Ctrl-E", + "itemize": "Ctrl-I" + }, + "labels_anchors": false, + "latex_user_defs": false, + "report_style_numbering": false, + "user_envs_cfg": false + }, + "toc": { + "base_numbering": 1, + "nav_menu": {}, + "number_sections": true, + "sideBar": true, + "skip_h1_title": false, + "title_cell": "Table of Contents", + "title_sidebar": "Contents", + "toc_cell": false, + "toc_position": {}, + "toc_section_display": true, + "toc_window_display": false + }, + "varInspector": { + "cols": { + "lenName": 16, + "lenType": 16, + "lenVar": 40 + }, + "kernels_config": { + "python": { + "delete_cmd_postfix": "", + "delete_cmd_prefix": "del ", + "library": "var_list.py", + "varRefreshCmd": "print(var_dic_list())" + }, + "r": { + "delete_cmd_postfix": ") ", + "delete_cmd_prefix": "rm(", + "library": "var_list.r", + "varRefreshCmd": "cat(var_dic_list()) " + } + }, + "types_to_exclude": [ + "module", + "function", + "builtin_function_or_method", + "instance", + "_Feature" + ], + "window_display": false + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/tests/test_env_observation_builder.py b/tests/test_env_observation_builder.py index db264c2975b75f32c2612aa19c0511076460ec6b..55c229e88e73c311ae8f8f4aeee01218cf1dd4cf 100644 --- a/tests/test_env_observation_builder.py +++ b/tests/test_env_observation_builder.py @@ -46,14 +46,14 @@ def test_global_obs(): double_switch_south_horizontal_straight, 180) rail_map = np.array( - [[empty] * 3 + [dead_end_from_south] + [empty] * 6] + - [[empty] * 3 + [vertical_straight] + [empty] * 6] * 2 + - [[dead_end_from_east] + [horizontal_straight] * 2 + - [double_switch_north_horizontal_straight] + - [horizontal_straight] * 2 + [double_switch_south_horizontal_straight] + - [horizontal_straight] * 2 + [dead_end_from_west]] + - [[empty] * 6 + [vertical_straight] + [empty] * 3] * 2 + - [[empty] * 6 + [dead_end_from_north] + [empty] * 3], dtype=np.uint16) + [[empty] * 3 + [dead_end_from_south] + [empty] * 6] + + [[empty] * 3 + [vertical_straight] + [empty] * 6] * 2 + + [[dead_end_from_east] + [horizontal_straight] * 2 + + [double_switch_north_horizontal_straight] + + [horizontal_straight] * 2 + [double_switch_south_horizontal_straight] + + [horizontal_straight] * 2 + [dead_end_from_west]] + + [[empty] * 6 + [vertical_straight] + [empty] * 3] * 2 + + [[empty] * 6 + [dead_end_from_north] + [empty] * 3], dtype=np.uint16) rail = GridTransitionMap(width=rail_map.shape[1], height=rail_map.shape[0], transitions=transitions) diff --git a/tests/test_environments.py b/tests/test_environments.py index ea8748b8aa4b50a1371a013be98f3b42d0d01228..210f1c76c8fd9978141a48189d5bcf2e31e68611 100644 --- a/tests/test_environments.py +++ b/tests/test_environments.py @@ -30,8 +30,7 @@ def test_rail_environment_single_agent(): transitions = Grid4Transitions([]) vertical_line = cells[1] south_symmetrical_switch = cells[6] - north_symmetrical_switch = transitions.rotate_transition( - south_symmetrical_switch, 180) + north_symmetrical_switch = transitions.rotate_transition(south_symmetrical_switch, 180) # Simple turn not in the base transitions ? south_east_turn = int('0100000000000010', 2) south_west_turn = transitions.rotate_transition(south_east_turn, 90) diff --git a/tests/test_rendertools.py b/tests/test_rendertools.py index 528cc59bd8e66b9d383d093c7dd6363e9dc45f71..1f5c317965a3101c6232709ebb311959d5a566ed 100644 --- a/tests/test_rendertools.py +++ b/tests/test_rendertools.py @@ -11,7 +11,7 @@ import os import matplotlib.pyplot as plt import flatland.utils.rendertools as rt -from flatland.core.env_observation_builder import GlobalObsForRailEnv, TreeObsForRailEnv +from flatland.core.env_observation_builder import TreeObsForRailEnv def checkFrozenImage(sFileImage): @@ -31,7 +31,7 @@ def checkFrozenImage(sFileImage): bytesFrozenImage = bytesImage else: assert(bytesFrozenImage.shape == bytesImage.shape) - assert((np.sum(np.square(bytesFrozenImage-bytesImage)) / bytesFrozenImage.size) < 1e-3) + assert((np.sum(np.square(bytesFrozenImage - bytesImage)) / bytesFrozenImage.size) < 1e-3) def test_render_env(): diff --git a/tests/test_transitions.py b/tests/test_transitions.py index f68b836e58b87078ef8fcf799ca089df0d09d292..0f56e886071fd1d217be03b9a7e875c20d1a0e8a 100644 --- a/tests/test_transitions.py +++ b/tests/test_transitions.py @@ -15,65 +15,60 @@ def test_valid_railenv_transitions(): for i in range(2): assert(rail_env_trans.get_transitions( - int('1100110000110011', 2), i) == (1, 1, 0, 0)) + int('1100110000110011', 2), i) == (1, 1, 0, 0)) assert(rail_env_trans.get_transitions( - int('1100110000110011', 2), 2+i) == (0, 0, 1, 1)) + int('1100110000110011', 2), 2 + i) == (0, 0, 1, 1)) no_transition_cell = int('0000000000000000', 2) for i in range(4): assert(rail_env_trans.get_transitions( - no_transition_cell, i) == (0, 0, 0, 0)) + no_transition_cell, i) == (0, 0, 0, 0)) # Facing south, going south - north_south_transition = rail_env_trans.set_transitions( - no_transition_cell, 2, (0, 0, 1, 0)) + north_south_transition = rail_env_trans.set_transitions(no_transition_cell, 2, (0, 0, 1, 0)) assert(rail_env_trans.set_transition( - north_south_transition, 2, 2, 0) == no_transition_cell) + north_south_transition, 2, 2, 0) == no_transition_cell) assert(rail_env_trans.get_transition( - north_south_transition, 2, 2)) + north_south_transition, 2, 2)) # Facing north, going east south_east_transition = \ - rail_env_trans.set_transition( - no_transition_cell, 0, 1, 1) + rail_env_trans.set_transition(no_transition_cell, 0, 1, 1) assert(rail_env_trans.get_transition( - south_east_transition, 0, 1)) + south_east_transition, 0, 1)) # The opposite transitions are not feasible assert(not rail_env_trans.get_transition( - north_south_transition, 2, 0)) + north_south_transition, 2, 0)) assert(not rail_env_trans.get_transition( - south_east_transition, 2, 1)) + south_east_transition, 2, 1)) - east_west_transition = rail_env_trans.rotate_transition( - north_south_transition, 90) - north_west_transition = rail_env_trans.rotate_transition( - south_east_transition, 180) + east_west_transition = rail_env_trans.rotate_transition(north_south_transition, 90) + north_west_transition = rail_env_trans.rotate_transition(south_east_transition, 180) # Facing west, going west assert(rail_env_trans.get_transition( - east_west_transition, 3, 3)) + east_west_transition, 3, 3)) # Facing south, going west assert(rail_env_trans.get_transition( - north_west_transition, 2, 3)) + north_west_transition, 2, 3)) assert(south_east_transition == rail_env_trans.rotate_transition( - south_east_transition, 360)) + south_east_transition, 360)) def test_diagonal_transitions(): diagonal_trans_env = Grid8Transitions([]) # Facing north, going north-east - south_northeast_transition = int('01000000' + '0'*8*7, 2) + south_northeast_transition = int('01000000' + '0' * 8 * 7, 2) assert(diagonal_trans_env.get_transitions( - south_northeast_transition, 0) == (0, 1, 0, 0, 0, 0, 0, 0)) + south_northeast_transition, 0) == (0, 1, 0, 0, 0, 0, 0, 0)) # Allowing transition from north to southwest: Facing south, going SW north_southwest_transition = \ - diagonal_trans_env.set_transitions( - int('0' * 64, 2), 4, (0, 0, 0, 0, 0, 1, 0, 0)) + diagonal_trans_env.set_transitions(int('0' * 64, 2), 4, (0, 0, 0, 0, 0, 1, 0, 0)) assert(diagonal_trans_env.rotate_transition( - south_northeast_transition, 180) == north_southwest_transition) + south_northeast_transition, 180) == north_southwest_transition) diff --git a/tox.ini b/tox.ini index 54bc00406686c1ba45a1ded15b10e147c2919394..6e5ef99fe393ede204f109e3f080d54768c2cd39 100644 --- a/tox.ini +++ b/tox.ini @@ -8,7 +8,7 @@ python = [flake8] max-line-length = 120 -ignore = E128 E121 E126 E123 E133 E226 E241 E242 W504 W +ignore = E121 E126 E123 E128 E133 E226 E241 E242 E704 W293 W391 W503 W504 W505 [testenv:flake8] basepython = python