diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index a907c4a2a1e2bb9162dd957ad61bd2d16dde8dd0..fc9a7a40641e8ee27323e91ca5a4ac7f6883a3fd 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -24,6 +24,8 @@ before_script: tests: stage: tests + services: + - redis before_script: - apt update - apt install -y libgl1-mesa-glx xvfb graphviz libgraphviz-dev xdg-utils libcairo2-dev libjpeg-dev libgif-dev python-pyglet @@ -102,6 +104,8 @@ benchmarks: test_conda_setup: stage: integration_testing + services: + - redis before_script: - apt update - apt install -y libgl1-mesa-glx xvfb xdg-utils libcairo2-dev libjpeg-dev libgif-dev graphviz libgraphviz-dev python-pyglet diff --git a/env_data/tests/Test_9_Level_1.pkl b/env_data/tests/Test_9_Level_1.pkl new file mode 100644 index 0000000000000000000000000000000000000000..e18a542f8a9d9caf2cca8046be420181ff91ce26 Binary files /dev/null and b/env_data/tests/Test_9_Level_1.pkl differ diff --git a/env_data/tests/service_test/Test_0/Level_0.pkl b/env_data/tests/service_test/Test_0/Level_0.pkl new file mode 100644 index 0000000000000000000000000000000000000000..e18a542f8a9d9caf2cca8046be420181ff91ce26 Binary files /dev/null and b/env_data/tests/service_test/Test_0/Level_0.pkl differ diff --git a/env_data/tests/service_test/Test_0/Level_1.pkl b/env_data/tests/service_test/Test_0/Level_1.pkl new file mode 100644 index 0000000000000000000000000000000000000000..e18a542f8a9d9caf2cca8046be420181ff91ce26 Binary files /dev/null and b/env_data/tests/service_test/Test_0/Level_1.pkl differ diff --git a/env_data/tests/service_test/metadata.csv b/env_data/tests/service_test/metadata.csv new file mode 100644 index 0000000000000000000000000000000000000000..021cae21e0c70cb176848aaed2662787e672f983 --- /dev/null +++ b/env_data/tests/service_test/metadata.csv @@ -0,0 +1,3 @@ +test_id,env_id,n_agents,x_dim,y_dim,n_cities,max_rails_in_city,malfunction_interval,n_envs_run,seed,grid_mode,max_rails_between_cities,malfunction_duration_min,malfunction_duration_max,speed_ratios +Test_0,Level_0,5,25,25,2,3,50,50,11111,False,2,20,50,{1.0: 1.0} +Test_0,Level_1,5,25,25,2,3,50,50,11111,False,2,20,50,{1.0: 1.0} diff --git a/flatland/envs/agent_chains.py b/flatland/envs/agent_chains.py index a2792479c0877aef2b41a8b62af7d55f5ab46bb0..3e566ad0617a3c49ec69e1049be36231b705916f 100644 --- a/flatland/envs/agent_chains.py +++ b/flatland/envs/agent_chains.py @@ -12,6 +12,9 @@ class MotionCheck(object): """ def __init__(self): self.G = nx.DiGraph() + self.nDeadlocks = 0 + self.svDeadlocked = set() + def addAgent(self, iAg, rc1, rc2, xlabel=None): """ add an agent and its motion as row,col tuples of current and next position. @@ -60,6 +63,10 @@ class MotionCheck(object): return svStops def find_stop_preds(self, svStops=None): + """ Find the predecessors to a list of stopped agents (ie the nodes / vertices) + Returns the set of predecessors. + Includes "chained" predecessors. + """ if svStops is None: svStops = self.find_stops2() @@ -73,9 +80,10 @@ class MotionCheck(object): for oWCC in lWCC: #print("Component:", oWCC) + # Get the node details for this WCC in a subgraph Gwcc = self.G.subgraph(oWCC) - # Find all the stops in this chain + # Find all the stops in this chain or tree svCompStops = svStops.intersection(Gwcc) #print(svCompStops) @@ -91,11 +99,14 @@ class MotionCheck(object): lStops = list(iter_stops) svBlocked.update(lStops) + # the set of all the nodes/agents blocked by this set of stopped nodes return svBlocked def find_swaps(self): """ find all the swap conflicts where two agents are trying to exchange places. These appear as simple cycles of length 2. + These agents are necessarily deadlocked (since they can't change direction in flatland) - + meaning they will now be stuck for the rest of the episode. """ #svStops = self.find_stops2() llvLoops = list(nx.algorithms.cycles.simple_cycles(self.G)) @@ -109,30 +120,73 @@ class MotionCheck(object): """ pass + def block_preds(self, svStops, color="red"): + """ Take a list of stopped agents, and apply a stop color to any chains/trees + of agents trying to head toward those cells. + Count the number of agents blocked, ignoring those which are already marked. + (Otherwise it can double count swaps) + + """ + iCount = 0 + svBlocked = set() + # The reversed graph allows us to follow directed edges to find affected agents. + Grev = self.G.reverse() + for v in svStops: + + # Use depth-first-search to find a tree of agents heading toward the blocked cell. + lvPred = list(nx.traversal.dfs_postorder_nodes(Grev, source=v)) + svBlocked |= set(lvPred) + svBlocked.add(v) + #print("node:", v, "set", svBlocked) + # only count those not already marked + for v2 in [v]+lvPred: + if self.G.nodes[v2].get("color") != color: + self.G.nodes[v2]["color"] = color + iCount += 1 + + return svBlocked + + def find_conflicts(self): - svStops = self.find_stops2() # { u for u,v in nx.classes.function.selfloop_edges(self.G) } - #llvLoops = list(nx.algorithms.cycles.simple_cycles(self.G)) - #llvSwaps = [lvLoop for lvLoop in llvLoops if len(lvLoop) == 2 ] - #svSwaps = { v for lvSwap in llvSwaps for v in lvSwap } - svSwaps = self.find_swaps() - svBlocked = self.find_stop_preds(svStops.union(svSwaps)) + svStops = self.find_stops2() # voluntarily stopped agents - have self-loops + svSwaps = self.find_swaps() # deadlocks - adjacent head-on collisions + + # Block all swaps and their tree of predessors + self.svDeadlocked = self.block_preds(svSwaps, color="purple") + # Take the union of the above, and find all the predecessors + #svBlocked = self.find_stop_preds(svStops.union(svSwaps)) + + # Just look for the the tree of preds for each voluntarily stopped agent + svBlocked = self.find_stop_preds(svStops) + + # iterate the nodes v with their predecessors dPred (dict of nodes->{}) for (v, dPred) in self.G.pred.items(): - if v in svSwaps: - self.G.nodes[v]["color"] = "purple" - elif v in svBlocked: + # mark any swaps with purple - these are directly deadlocked + #if v in svSwaps: + # self.G.nodes[v]["color"] = "purple" + # If they are not directly deadlocked, but are in the union of stopped + deadlocked + #elif v in svBlocked: + + # if in blocked, it will not also be in a swap pred tree, so no need to worry about overwriting + if v in svBlocked: self.G.nodes[v]["color"] = "red" + # not blocked but has two or more predecessors, ie >=2 agents waiting to enter this node elif len(dPred)>1: - if self.G.nodes[v].get("color") == "red": + # if this agent is already red/blocked, ignore. CHECK: why? + # certainly we want to ignore purple so we don't overwrite with red. + if self.G.nodes[v].get("color") in ("red", "purple"): continue + # if this node has no agent, and >=2 want to enter it. if self.G.nodes[v].get("agent") is None: self.G.nodes[v]["color"] = "blue" + # this node has an agent and >=2 want to enter else: self.G.nodes[v]["color"] = "magenta" - # predecessors of a contended cell + # predecessors of a contended cell: {agent index -> node} diAgCell = {self.G.nodes[vPred].get("agent"): vPred for vPred in dPred} # remove the agent with the lowest index, who wins @@ -140,13 +194,15 @@ class MotionCheck(object): diAgCell.pop(iAgWinner) # Block all the remaining predessors, and their tree of preds - for iAg, v in diAgCell.items(): - self.G.nodes[v]["color"] = "red" - for vPred in nx.traversal.dfs_postorder_nodes(self.G.reverse(), source=v): - self.G.nodes[vPred]["color"] = "red" + #for iAg, v in diAgCell.items(): + # self.G.nodes[v]["color"] = "red" + # for vPred in nx.traversal.dfs_postorder_nodes(self.G.reverse(), source=v): + # self.G.nodes[vPred]["color"] = "red" + self.block_preds(diAgCell.values(), "red") def check_motion(self, iAgent, rcPos): - """ If agent position is None, we use a dummy position of (-1, iAgent) + """ Returns tuple of boolean can the agent move, and the cell it will move into. + If agent position is None, we use a dummy position of (-1, iAgent) """ if rcPos is None: @@ -168,7 +224,7 @@ class MotionCheck(object): # This should never happen - only the next cell of an agent has no successor if len(dSucc)==0: - print(f"error condition - agent {iAg} node {rcPos} has no successor") + print(f"error condition - agent {iAgent} node {rcPos} has no successor") return (False, rcPos) # This agent has a successor @@ -181,6 +237,7 @@ class MotionCheck(object): + def render(omc:MotionCheck, horizontal=True): try: oAG = nx.drawing.nx_agraph.to_agraph(omc.G) diff --git a/flatland/envs/malfunction_generators.py b/flatland/envs/malfunction_generators.py index 62a920805d2b530c5da2f26b9289e3a84e15cc02..0d27913d6f27fb5df301960655d90baa42ef1ac0 100644 --- a/flatland/envs/malfunction_generators.py +++ b/flatland/envs/malfunction_generators.py @@ -8,13 +8,17 @@ from numpy.random.mtrand import RandomState from flatland.envs.agent_utils import EnvAgent, RailAgentStatus from flatland.envs import persistence -Malfunction = NamedTuple('Malfunction', [('num_broken_steps', int)]) + +# why do we have both MalfunctionParameters and MalfunctionProcessData - they are both the same! MalfunctionParameters = NamedTuple('MalfunctionParameters', [('malfunction_rate', float), ('min_duration', int), ('max_duration', int)]) -MalfunctionGenerator = Callable[[EnvAgent, RandomState, bool], Optional[Malfunction]] MalfunctionProcessData = NamedTuple('MalfunctionProcessData', [('malfunction_rate', float), ('min_duration', int), ('max_duration', int)]) +Malfunction = NamedTuple('Malfunction', [('num_broken_steps', int)]) + +# Why is the return value Optional? We always return a Malfunction. +MalfunctionGenerator = Callable[[EnvAgent, RandomState, bool], Optional[Malfunction]] def _malfunction_prob(rate: float) -> float: """ @@ -27,6 +31,146 @@ def _malfunction_prob(rate: float) -> float: else: return 1 - np.exp(-rate) +class ParamMalfunctionGen(object): + """ Preserving old behaviour of using MalfunctionParameters for constructor, + but returning MalfunctionProcessData in get_process_data. + Data structure and content is the same. + """ + def __init__(self, parameters: MalfunctionParameters): + #self.mean_malfunction_rate = parameters.malfunction_rate + #self.min_number_of_steps_broken = parameters.min_duration + #self.max_number_of_steps_broken = parameters.max_duration + self.MFP = parameters + + def generate(self, + agent: EnvAgent = None, + np_random: RandomState = None, + reset=False) -> Optional[Malfunction]: + + # Dummy reset function as we don't implement specific seeding here + if reset: + return Malfunction(0) + + if agent.malfunction_data['malfunction'] < 1: + if np_random.rand() < _malfunction_prob(self.MFP.malfunction_rate): + num_broken_steps = np_random.randint(self.MFP.min_duration, + self.MFP.max_duration + 1) + 1 + return Malfunction(num_broken_steps) + return Malfunction(0) + + def get_process_data(self): + return MalfunctionProcessData(*self.MFP) + + +class NoMalfunctionGen(ParamMalfunctionGen): + def __init__(self): + super().__init__(MalfunctionParameters(0,0,0)) + +class FileMalfunctionGen(ParamMalfunctionGen): + def __init__(self, env_dict=None, filename=None, load_from_package=None): + """ uses env_dict if populated, otherwise tries to load from file / package. + """ + if env_dict is None: + env_dict = persistence.RailEnvPersister.load_env_dict(filename, load_from_package=load_from_package) + + if "malfunction" in env_dict: + oMFP = MalfunctionParameters(*env_dict["malfunction"]) + else: + oMFP = MalfunctionParameters(0,0,0) # no malfunctions + super().__init__(oMFP) + + +################################################################################################ +# OLD / DEPRECATED generator functions below. To be removed. + +def no_malfunction_generator() -> Tuple[MalfunctionGenerator, MalfunctionProcessData]: + """ + Malfunction generator which generates no malfunctions + + Parameters + ---------- + Nothing + + Returns + ------- + generator, Tuple[float, int, int] with mean_malfunction_rate, min_number_of_steps_broken, max_number_of_steps_broken + """ + print("DEPRECATED - use NoMalfunctionGen instead of no_malfunction_generator") + # Mean malfunction in number of time steps + mean_malfunction_rate = 0. + + # Uniform distribution parameters for malfunction duration + min_number_of_steps_broken = 0 + max_number_of_steps_broken = 0 + + def generator(agent: EnvAgent = None, np_random: RandomState = None, reset=False) -> Optional[Malfunction]: + return Malfunction(0) + + return generator, MalfunctionProcessData(mean_malfunction_rate, min_number_of_steps_broken, + max_number_of_steps_broken) + + + +def single_malfunction_generator(earlierst_malfunction: int, malfunction_duration: int) -> Tuple[ + MalfunctionGenerator, MalfunctionProcessData]: + """ + Malfunction generator which guarantees exactly one malfunction during an episode of an ACTIVE agent. + + Parameters + ---------- + earlierst_malfunction: Earliest possible malfunction onset + malfunction_duration: The duration of the single malfunction + + Returns + ------- + generator, Tuple[float, int, int] with mean_malfunction_rate, min_number_of_steps_broken, max_number_of_steps_broken + """ + # Mean malfunction in number of time steps + mean_malfunction_rate = 0. + + # Uniform distribution parameters for malfunction duration + min_number_of_steps_broken = 0 + max_number_of_steps_broken = 0 + + # Keep track of the total number of malfunctions in the env + global_nr_malfunctions = 0 + + # Malfunction calls per agent + malfunction_calls = dict() + + def generator(agent: EnvAgent = None, np_random: RandomState = None, reset=False) -> Optional[Malfunction]: + # We use the global variable to assure only a single malfunction in the env + nonlocal global_nr_malfunctions + nonlocal malfunction_calls + + # Reset malfunciton generator + if reset: + nonlocal global_nr_malfunctions + nonlocal malfunction_calls + global_nr_malfunctions = 0 + malfunction_calls = dict() + return Malfunction(0) + + # No more malfunctions if we already had one, ignore all updates + if global_nr_malfunctions > 0: + return Malfunction(0) + + # Update number of calls per agent + if agent.handle in malfunction_calls: + malfunction_calls[agent.handle] += 1 + else: + malfunction_calls[agent.handle] = 1 + + # Break an agent that is active at the time of the malfunction + if agent.status == RailAgentStatus.ACTIVE and malfunction_calls[agent.handle] >= earlierst_malfunction: + global_nr_malfunctions += 1 + return Malfunction(malfunction_duration) + else: + return Malfunction(0) + + return generator, MalfunctionProcessData(mean_malfunction_rate, min_number_of_steps_broken, + max_number_of_steps_broken) + def malfunction_from_file(filename: str, load_from_package=None) -> Tuple[MalfunctionGenerator, MalfunctionProcessData]: """ @@ -40,13 +184,9 @@ def malfunction_from_file(filename: str, load_from_package=None) -> Tuple[Malfun ------- generator, Tuple[float, int, int] with mean_malfunction_rate, min_number_of_steps_broken, max_number_of_steps_broken """ - # with open(filename, "rb") as file_in: - # load_data = file_in.read() - # if filename.endswith("mpk"): - # data = msgpack.unpackb(load_data, use_list=False, encoding='utf-8') - # elif filename.endswith("pkl"): - # data = pickle.loads(load_data) + print("DEPRECATED - use FileMalfunctionGen instead of malfunction_from_file") + env_dict = persistence.RailEnvPersister.load_env_dict(filename, load_from_package=load_from_package) # TODO: make this better by using namedtuple in the pickle file. See issue 282 if "malfunction" in env_dict: @@ -111,6 +251,9 @@ def malfunction_from_params(parameters: MalfunctionParameters) -> Tuple[Malfunct ------- generator, Tuple[float, int, int] with mean_malfunction_rate, min_number_of_steps_broken, max_number_of_steps_broken """ + + print("DEPRECATED - use ParamMalfunctionGen instead of malfunction_from_params") + mean_malfunction_rate = parameters.malfunction_rate min_number_of_steps_broken = parameters.min_duration max_number_of_steps_broken = parameters.max_duration @@ -142,128 +285,3 @@ def malfunction_from_params(parameters: MalfunctionParameters) -> Tuple[Malfunct return generator, MalfunctionProcessData(mean_malfunction_rate, min_number_of_steps_broken, max_number_of_steps_broken) - -class ParamMalfunctionGen(object): - def __init__(self, parameters: MalfunctionParameters): - self.mean_malfunction_rate = parameters.malfunction_rate - self.min_number_of_steps_broken = parameters.min_duration - self.max_number_of_steps_broken = parameters.max_duration - - def generate(self, agent: EnvAgent = None, np_random: RandomState = None, reset=False) -> Optional[Malfunction]: - - # Dummy reset function as we don't implement specific seeding here - if reset: - return Malfunction(0) - - if agent.malfunction_data['malfunction'] < 1: - if np_random.rand() < _malfunction_prob(self.mean_malfunction_rate): - num_broken_steps = np_random.randint(self.min_number_of_steps_broken, - self.max_number_of_steps_broken + 1) + 1 - return Malfunction(num_broken_steps) - return Malfunction(0) - - def get_process_data(self): - return MalfunctionProcessData( - self.mean_malfunction_rate, - self.min_number_of_steps_broken, - self.max_number_of_steps_broken) - - -class NoMalfunctionGen(ParamMalfunctionGen): - def __init__(self): - self.mean_malfunction_rate = 0. - self.min_number_of_steps_broken = 0 - self.max_number_of_steps_broken = 0 - - def generate(self, agent: EnvAgent = None, np_random: RandomState = None, reset=False) -> Optional[Malfunction]: - return Malfunction(0) - - - - -def no_malfunction_generator() -> Tuple[MalfunctionGenerator, MalfunctionProcessData]: - """ - Malfunction generator which generates no malfunctions - - Parameters - ---------- - Nothing - - Returns - ------- - generator, Tuple[float, int, int] with mean_malfunction_rate, min_number_of_steps_broken, max_number_of_steps_broken - """ - # Mean malfunction in number of time steps - mean_malfunction_rate = 0. - - # Uniform distribution parameters for malfunction duration - min_number_of_steps_broken = 0 - max_number_of_steps_broken = 0 - - def generator(agent: EnvAgent = None, np_random: RandomState = None, reset=False) -> Optional[Malfunction]: - return Malfunction(0) - - return generator, MalfunctionProcessData(mean_malfunction_rate, min_number_of_steps_broken, - max_number_of_steps_broken) - - - -def single_malfunction_generator(earlierst_malfunction: int, malfunction_duration: int) -> Tuple[ - MalfunctionGenerator, MalfunctionProcessData]: - """ - Malfunction generator which guarantees exactly one malfunction during an episode of an ACTIVE agent. - - Parameters - ---------- - earlierst_malfunction: Earliest possible malfunction onset - malfunction_duration: The duration of the single malfunction - - Returns - ------- - generator, Tuple[float, int, int] with mean_malfunction_rate, min_number_of_steps_broken, max_number_of_steps_broken - """ - # Mean malfunction in number of time steps - mean_malfunction_rate = 0. - - # Uniform distribution parameters for malfunction duration - min_number_of_steps_broken = 0 - max_number_of_steps_broken = 0 - - # Keep track of the total number of malfunctions in the env - global_nr_malfunctions = 0 - - # Malfunction calls per agent - malfunction_calls = dict() - - def generator(agent: EnvAgent = None, np_random: RandomState = None, reset=False) -> Optional[Malfunction]: - # We use the global variable to assure only a single malfunction in the env - nonlocal global_nr_malfunctions - nonlocal malfunction_calls - - # Reset malfunciton generator - if reset: - nonlocal global_nr_malfunctions - nonlocal malfunction_calls - global_nr_malfunctions = 0 - malfunction_calls = dict() - return Malfunction(0) - - # No more malfunctions if we already had one, ignore all updates - if global_nr_malfunctions > 0: - return Malfunction(0) - - # Update number of calls per agent - if agent.handle in malfunction_calls: - malfunction_calls[agent.handle] += 1 - else: - malfunction_calls[agent.handle] = 1 - - # Break an agent that is active at the time of the malfunction - if agent.status == RailAgentStatus.ACTIVE and malfunction_calls[agent.handle] >= earlierst_malfunction: - global_nr_malfunctions += 1 - return Malfunction(malfunction_duration) - else: - return Malfunction(0) - - return generator, MalfunctionProcessData(mean_malfunction_rate, min_number_of_steps_broken, - max_number_of_steps_broken) diff --git a/flatland/envs/persistence.py b/flatland/envs/persistence.py index 1b0f05f14b57e50f0c9b9021a36a6b58a77ddb17..bc4b169b1aad3893d96d82cb8284b369f13104f2 100644 --- a/flatland/envs/persistence.py +++ b/flatland/envs/persistence.py @@ -124,8 +124,9 @@ class RailEnvPersister(object): load_from_package=load_from_package), schedule_generator=sched_gen.schedule_from_file(filename, load_from_package=load_from_package), - malfunction_generator_and_process_data=mal_gen.malfunction_from_file(filename, - load_from_package=load_from_package), + #malfunction_generator_and_process_data=mal_gen.malfunction_from_file(filename, + # load_from_package=load_from_package), + malfunction_generator=mal_gen.FileMalfunctionGen(env_dict), obs_builder_object=DummyObservationBuilder(), record_steps=True) diff --git a/flatland/envs/rail_env.py b/flatland/envs/rail_env.py index d9ff7120789d5ad58ba4acc15d16586f0e34b827..94d911eaab8dfe545d6960f9cc7068e53fee8e16 100644 --- a/flatland/envs/rail_env.py +++ b/flatland/envs/rail_env.py @@ -728,6 +728,8 @@ class RailEnv(Environment): self.rewards_dict[i_agent] += self.step_penalty * agent.speed_data['speed'] def _step_agent_cf(self, i_agent, action: Optional[RailEnvActions] = None): + """ "close following" version of step_agent. + """ agent = self.agents[i_agent] if agent.status in [RailAgentStatus.DONE, RailAgentStatus.DONE_REMOVED]: # this agent has already completed... return @@ -748,6 +750,8 @@ class RailEnv(Environment): # if agent is broken, actions are ignored and agent does not move. # full step penalty in this case + # TODO: this means that deadlocked agents which suffer a malfunction are marked as + # stopped rather than deadlocked. if agent.malfunction_data['malfunction'] > 0: self.motionCheck.addAgent(i_agent, agent.position, agent.position) # agent will get penalty in step_agent2_cf @@ -999,7 +1003,8 @@ class RailEnv(Environment): list_agents_state.append([ *pos, int(agent.direction), agent.malfunction_data["malfunction"], - int(agent.status) + int(agent.status), + int(agent.position in self.motionCheck.svDeadlocked) ]) self.cur_episode.append(list_agents_state) diff --git a/flatland/evaluators/service.py b/flatland/evaluators/service.py index 06e2a75e3770f21c26c2db629fe225278e5bc522..d77f8cbbd6a9d40c172c20d386dda174cd6bc19a 100644 --- a/flatland/evaluators/service.py +++ b/flatland/evaluators/service.py @@ -55,7 +55,7 @@ TEST_MIN_PERCENTAGE_COMPLETE_MEAN = 0.25 # After this number of consecutive timeouts, kill the submission: # this probably means the submission has crashed -MAX_SUCCESSIVE_TIMEOUTS = 10 +MAX_SUCCESSIVE_TIMEOUTS = int(os.getenv("FLATLAND_MAX_SUCCESSIVE_TIMEOUTS", 10)) debug_mode = (os.getenv("AICROWD_DEBUG_SUBMISSION", 0) == 1) if debug_mode: @@ -163,7 +163,7 @@ class FlatlandRemoteEvaluationService: self.test_env_folder = test_env_folder self.video_generation_envs = video_generation_envs self.env_file_paths = self.get_env_filepaths() - + print(self.env_file_paths) # Shuffle all the env_file_paths for more exciting videos # and for more uniform time progression if shuffle: @@ -240,6 +240,7 @@ class FlatlandRemoteEvaluationService: self.simulation_times = [] self.env_step_times = [] self.nb_malfunctioning_trains = [] + self.nb_deadlocked_trains = [] self.overall_start_time = 0 self.termination_cause = "No reported termination cause." self.evaluation_done = False @@ -391,6 +392,8 @@ class FlatlandRemoteEvaluationService: self.evaluation_metadata_df["steps"] = np.nan self.evaluation_metadata_df["simulation_time"] = np.nan self.evaluation_metadata_df["nb_malfunctioning_trains"] = np.nan + self.evaluation_metadata_df["nb_deadlocked_trains"] = np.nan + # Add client specific columns # TODO: This needs refactoring @@ -414,34 +417,61 @@ class FlatlandRemoteEvaluationService: last_simulation_env_file_path ] - _row.reward = self.simulation_rewards[-1] - _row.normalized_reward = self.simulation_rewards_normalized[-1] - _row.percentage_complete = self.simulation_percentage_complete[-1] - _row.steps = self.simulation_steps[-1] - _row.simulation_time = self.simulation_times[-1] - _row.nb_malfunctioning_trains = self.nb_malfunctioning_trains[-1] - - # TODO: This needs refactoring # Add controller_inference_time_metrics # These metrics may be missing if no step was done before the episode finished - if "current_episode_controller_inference_time_min" in self.stats: - _row.controller_inference_time_min = self.stats[ - "current_episode_controller_inference_time_min" - ] - _row.controller_inference_time_mean = self.stats[ - "current_episode_controller_inference_time_mean" - ] - _row.controller_inference_time_max = self.stats[ - "current_episode_controller_inference_time_max" - ] - else: - _row.controller_inference_time_min = 0.0 - _row.controller_inference_time_mean = 0.0 - _row.controller_inference_time_max = 0.0 - self.evaluation_metadata_df.loc[ - last_simulation_env_file_path - ] = _row + # generate the lists of names for the stats (input names and output names) + sPrefixIn = "current_episode_controller_inference_time_" + sPrefixOut = "controller_inference_time_" + lsStatIn = [ sPrefixIn + sStat for sStat in ["min", "mean", "max"] ] + lsStatOut = [ sPrefixOut + sStat for sStat in ["min", "mean", "max"] ] + + if lsStatIn[0] in self.stats: + lrStats = [ self.stats[sStat] for sStat in lsStatIn ] + else: + lrStats = [ 0.0 ] * len(lsStatIn) + + lsFields = ("reward, normalized_reward, percentage_complete, " +\ + "steps, simulation_time, nb_malfunctioning_trains, nb_deadlocked_trains").split(", ") +\ + lsStatOut + + loValues = [ self.simulation_rewards[-1], + self.simulation_rewards_normalized[-1], + self.simulation_percentage_complete[-1], + self.simulation_steps[-1], + self.simulation_times[-1], + self.nb_malfunctioning_trains[-1], + self.nb_deadlocked_trains[-1] + ] + lrStats + + # update the dataframe without the updating-a-copy warning + df = self.evaluation_metadata_df + df.loc[last_simulation_env_file_path, lsFields] = loValues + + #_row.reward = self.simulation_rewards[-1] + #_row.normalized_reward = self.simulation_rewards_normalized[-1] + #_row.percentage_complete = self.simulation_percentage_complete[-1] + #_row.steps = self.simulation_steps[-1] + #_row.simulation_time = self.simulation_times[-1] + #_row.nb_malfunctioning_trains = self.nb_malfunctioning_trains[-1] + + #_row.controller_inference_time_min = self.stats[ + # "current_episode_controller_inference_time_min" + #] + #_row.controller_inference_time_mean = self.stats[ + # "current_episode_controller_inference_time_mean" + #] + #_row.controller_inference_time_max = self.stats[ + # "current_episode_controller_inference_time_max" + #] + #else: + # _row.controller_inference_time_min = 0.0 + # _row.controller_inference_time_mean = 0.0 + # _row.controller_inference_time_max = 0.0 + + #self.evaluation_metadata_df.loc[ + # last_simulation_env_file_path + #] = _row # Delete this key from the stats to ensure that it # gets computed again from scratch in the next episode @@ -642,8 +672,15 @@ class FlatlandRemoteEvaluationService: # reset the timeout flag / state. self.state_env_timed_out = False - test_env_file_path = self.env_file_paths[self.simulation_count] - env_test, env_level = self.get_env_test_and_level(test_env_file_path) + # Check if we have finished all the available envs + if self.simulation_count >= len(self.env_file_paths): + self.evaluation_done = True + # Hack - just ensure these are set + test_env_file_path = self.env_file_paths[self.simulation_count-1] + env_test, env_level = self.get_env_test_and_level(test_env_file_path) + else: + test_env_file_path = self.env_file_paths[self.simulation_count] + env_test, env_level = self.get_env_test_and_level(test_env_file_path) # Did we just finish a test, and if yes did it reach high enough mean percentage done? if self.current_test != env_test and env_test != 0: @@ -679,12 +716,8 @@ class FlatlandRemoteEvaluationService: self.current_level = env_level del self.env - self.env = RailEnv(width=1, height=1, - rail_generator=rail_from_file(test_env_file_path), - schedule_generator=schedule_from_file(test_env_file_path), - malfunction_generator_and_process_data=malfunction_from_file(test_env_file_path), - obs_builder_object=DummyObservationBuilder(), - record_steps=True) + + self.env, _env_dict = RailEnvPersister.load_new(test_env_file_path) self.begin_simulation = time.time() @@ -866,13 +899,20 @@ class FlatlandRemoteEvaluationService: print("Percentage for test {}, level {}: {}".format(self.current_test, self.current_level, percentage_complete)) print(self.simulation_percentage_complete_per_test[self.current_test]) + if len(self.env.cur_episode) > 0: + g3Ep = np.array(self.env.cur_episode) + self.nb_deadlocked_trains.append(np.sum(g3Ep[-1,:,5])) + else: + self.nb_deadlocked_trains.append(np.nan) + print( "Evaluation finished in {} timesteps, {:.3f} seconds. Percentage agents done: {:.3f}. Normalized reward: {:.3f}. Number of malfunctions: {}.".format( self.simulation_steps[-1], self.simulation_times[-1], self.simulation_percentage_complete[-1], self.simulation_rewards_normalized[-1], - self.nb_malfunctioning_trains[-1] + self.nb_malfunctioning_trains[-1], + self.nb_deadlocked_trains[-1] )) print("Total normalized reward so far: {:.3f}".format(sum(self.simulation_rewards_normalized))) @@ -1101,13 +1141,17 @@ class FlatlandRemoteEvaluationService: _command_response = {} _command_response['type'] = messages.FLATLAND_RL.ERROR _command_response['payload'] = error_message - _redis.rpush( - command_response_channel, - msgpack.packb( + + if self.use_pickle: + bytes_error = pickle.dumps(_command_response) + else: + bytes_error = msgpack.packb( _command_response, default=m.encode, use_bin_type=True) - ) + + _redis.rpush(command_response_channel, bytes_error) + self.evaluation_state["state"] = "ERROR" self.evaluation_state["error"] = error_message self.evaluation_state["meta"]["termination_cause"] = "An error occured." @@ -1177,6 +1221,15 @@ class FlatlandRemoteEvaluationService: print(msg, "Evaluation will stop.") self.termination_cause = msg self.evaluation_done = True + # JW - change the command to a submit + print("Creating fake submit message after excessive timeouts.") + command = { + "type":messages.FLATLAND_RL.ENV_SUBMIT, + "payload": {}, + "response_channel": self.previous_command.get("response_channel") } + + return self.handle_env_submit(command) + continue self.timeout_counter = 0 diff --git a/flatland/utils/env_edit_utils.py b/flatland/utils/env_edit_utils.py index bf6aa32a6c0d6050acd2c28de7177b4d0bade6bf..98a22b809d668a9062646c7ca3644b0c35c2092f 100644 --- a/flatland/utils/env_edit_utils.py +++ b/flatland/utils/env_edit_utils.py @@ -59,7 +59,8 @@ def makeEnv2(nAg=2, shape=(20,10), llrcPaths=[], lrcStarts=[], lrcTargs=[], liDi number_of_agents=nAg, schedule_generator=oSG, obs_builder_object=obs.TreeObsForRailEnv(max_depth=1), - close_following=bUCF) + close_following=bUCF, + record_steps=True) envModel = editor.EditorModel(env) env.reset() diff --git a/flatland/utils/jupyter_utils.py b/flatland/utils/jupyter_utils.py index 3b7bc3e0c69310bf2696aa1831219f178cd5b500..f28f07af1142d8e7029aa7553d6c7a5c667f46b3 100644 --- a/flatland/utils/jupyter_utils.py +++ b/flatland/utils/jupyter_utils.py @@ -29,6 +29,16 @@ class AlwaysForward(Behaviour): def getActions(self): return { i:RailEnvActions.MOVE_FORWARD for i in range(self.nAg) } +class DelayedStartForward(AlwaysForward): + def __init__(self, env, nStartDelay=2): + self.nStartDelay = nStartDelay + super().__init__(env) + + def getActions(self): + iStep = self.env._elapsed_steps + 1 + nAgentsMoving = min(self.nAg, iStep // self.nStartDelay) + return { i:RailEnvActions.MOVE_FORWARD for i in range(nAgentsMoving) } + AgentPause = NamedTuple("AgentPause", [ ("iAg", int), diff --git a/notebooks/Agent-Close-Following.ipynb b/notebooks/Agent-Close-Following.ipynb index 41986e7fa8a90e1b49cea01aaca14950a999126a..069052747e91dadea4c1ec0afd9475fe9d2fd9db 100644 --- a/notebooks/Agent-Close-Following.ipynb +++ b/notebooks/Agent-Close-Following.ipynb @@ -73,7 +73,9 @@ "import networkx as nx\n", "import PIL\n", "from IPython import display\n", - "import time" + "import time\n", + "from matplotlib import pyplot as plt\n", + "import numpy as np" ] }, { @@ -153,7 +155,8 @@ "metadata": {}, "outputs": [], "source": [ - "import graphviz" + "#for v, dPred in omc.G.pred.items():\n", + "# print (v, dPred)" ] }, { @@ -162,10 +165,7 @@ "metadata": {}, "outputs": [], "source": [ - "oAG = nx.drawing.nx_agraph.to_agraph(omc.G)\n", - "oAG.layout(\"dot\")\n", - "sDot = oAG.to_string()\n", - "oSrc = graphviz.Source(sDot)" + "#import graphviz" ] }, { @@ -174,7 +174,10 @@ "metadata": {}, "outputs": [], "source": [ - "sSVG = oSrc._repr_svg_()" + "#oAG = nx.drawing.nx_agraph.to_agraph(omc.G)\n", + "#oAG.layout(\"dot\")\n", + "#sDot = oAG.to_string()\n", + "#oSrc = graphviz.Source(sDot)" ] }, { @@ -183,7 +186,16 @@ "metadata": {}, "outputs": [], "source": [ - "display.SVG(sSVG)" + "#sSVG = oSrc._repr_svg_()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#display.SVG(sSVG)" ] }, { @@ -251,6 +263,124 @@ " dAgState[iAg] = (*ag.position, ag.direction)\n", "assert dAgState == dAgStateFrozen\n" ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "env.motionCheck.svDeadlocked" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Deadlocking agents\n", + "We have added deadlock detection in the close-following code. This detects agents which are head-to-head ie facing in opposing directions in adjacent squares, and thus will not be able to move for the rest of the episode. This deadlocked status is propagated to any other agents which are blocked by the opposing pair.\n", + "\n", + "In the example below, agents 0 and 1 collide head on. The other agents are spaced out behind them and collide into them in subsequent steps.\n", + "\n", + "The deadlock status is now recorded in element 5 of each agent in the recorded episode. (row, col, dir, status, malfunction, deadlock)\n", + "\n", + "__Bugs / Limitations__\n", + "\n", + "The code does not currently count agents which are deadlocked, if they are also malfunctioning, or choose to stop moving voluntarily.\n", + "\n", + "The code does not detect agents which are about to become deadlocked, because they are heading towards each other on a track with no junctions or relevant targets." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "env, envModel = eeu.makeTestEnv(\"loop_with_loops\", nAg=10, bUCF=True)\n", + "oEC = ju.EnvCanvas(env, behaviour=ju.DelayedStartForward(env, nStartDelay=1))\n", + "oEC.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "for i in range(25):\n", + " oEC.step()\n", + " oEC.render()\n", + " \n", + " #display.display_html(f\"<br>Step: {i}\\n\", raw=True)\n", + " #display.display_svg(ac.render(env.motionCheck, horizontal=(i>=3)))\n", + " time.sleep(0.1) " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "env.motionCheck.svDeadlocked" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "g3Ep = np.array(env.cur_episode)\n", + "g3Ep.shape" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "nSteps = g3Ep.shape[0]\n", + "plt.step(range(nSteps), np.sum(g3Ep[:,:,5], axis=1))\n", + "plt.title(\"Deadlocked agents\")\n", + "plt.xticks(range(g3Ep.shape[0]))\n", + "plt.yticks(range(11))\n", + "plt.grid()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "gnDeadlockExpected = np.array([ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 5, 7, 9, 10, 10, 10, 10])\n", + "gnDeadlock = np.sum(g3Ep[:,:,5], axis=1)\n", + "\n", + "assert np.all(gnDeadlock == gnDeadlockExpected), \"Deadlocks by step do not match expected values!\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "cmap = plt.get_cmap(\"summer\")\n", + "nT, nAg, _ = g3Ep.shape\n", + "plt.pcolormesh(g3Ep[:,:,5].T, cmap=cmap)\n", + "plt.xlabel(\"Time Step\")\n", + "plt.xticks(range(0,nT, 2))\n", + "plt.ylabel(\"Agent index\")\n", + "plt.yticks(range(nAg))\n", + "\n", + "\n", + "plt.colorbar(ticks=[0,1], fraction=0.018, pad=0.1)\n", + "plt.title(\"Deadlock status\")\n", + "plt.grid()" + ] } ], "metadata": { diff --git a/notebooks/notebook-list b/notebooks/notebook-list index 5bb27272753452600245f3930cf2b6e4db82b566..794db00689f4154c3d1b3148add85492adffa3ba 100644 --- a/notebooks/notebook-list +++ b/notebooks/notebook-list @@ -9,3 +9,4 @@ simple_example_3_manual_control.ipynb Simple_Rendering_Demo.ipynb test-collision.ipynb test-saved-envs.ipynb +test-service.ipynb diff --git a/notebooks/render-episode.ipynb b/notebooks/render-episode.ipynb index 59d55b25a59ef0e426faecfc2de5b043cab72deb..d663c639ec42f4ea98b1d1669e827d02e03715b4 100644 --- a/notebooks/render-episode.ipynb +++ b/notebooks/render-episode.ipynb @@ -116,7 +116,9 @@ "id": "UeX1h4c0i5e6" }, "source": [ - "# Experiments" + "# Experiments\n", + "\n", + "This has been mostly changed to load envs using `importlib_resources`. It's getting them from the package \"envdata.tests`" ] }, { @@ -153,7 +155,8 @@ "metadata": {}, "outputs": [], "source": [ - "sPack, sResource = \"env_data.tests\", \"Test_2_Level_0.pkl\"" + "#sPack, sResource = \"env_data.tests\", \"Test_2_Level_0.pkl\"\n", + "sPack, sResource = \"env_data.tests\", \"Test_9_Level_1.pkl\"" ] }, { @@ -202,9 +205,10 @@ "outputs": [], "source": [ "env, env_dict = RailEnvPersister.load_new(sResource, load_from_package=sPack) # env_file)\n", + "\n", + "# the seed has to match that used to record the episode, in order for the malfunctions to match.\n", "env.reset(random_seed=1001)\n", "oRT = RenderTool(env, show_debug=True)\n", - "\n", "aImg = oRT.render_env(show_rowcols=True, return_image=True, show_inactive_agents=True)\n", "print(env._max_episode_steps)\n", "PIL.Image.fromarray(aImg)\n" @@ -327,8 +331,9 @@ "\n", "step = 0\n", "all_done = False\n", + "failed_action_check = False\n", "print(\"Processing episode steps:\")\n", - "while not all_done and step < max_steps:\n", + "while not all_done and step < len(expert_actions):\n", " print(step, end=\", \")\n", " \"\"\"\n", " for a in range(n_agents):\n", @@ -344,10 +349,12 @@ " action_dict.update({a: action})\n", " \"\"\"\n", " \n", - " dAct = expert_actions[step]\n", - " #print(dAct)\n", " if step < len(expert_actions):\n", - " next_obs, all_rewards, done, info = env.step(expert_actions[step])\n", + " dAct = expert_actions[step]\n", + " else:\n", + " dAct = {}\n", + " \n", + " next_obs, all_rewards, done, info = env.step(dAct)\n", " \n", " if True:\n", " # Check that agent states match recorded states\n", @@ -355,6 +362,7 @@ " pass\n", " else:\n", " print(\"MISMATCH\")\n", + " failed_action_check = True\n", " #print(\"env:\", get_agent_state(env))\n", " #print(\"epi:\", episode_states[step])\n", " llAgSt = get_agent_state(env)\n", @@ -402,6 +410,15 @@ " step += 1" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "assert failed_action_check == False, \"Realised states did not match stored states.\"" + ] + }, { "cell_type": "code", "execution_count": null, diff --git a/notebooks/test-service.ipynb b/notebooks/test-service.ipynb new file mode 100644 index 0000000000000000000000000000000000000000..821cb63a2c48e7d34795be0679406e0606a9a682 --- /dev/null +++ b/notebooks/test-service.ipynb @@ -0,0 +1,1292 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Test Service\n", + "\n", + "Intended to test the service.py evaluator.\n", + "Runs the service.py and a simple client.\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "baXcVq3ii0Cb" + }, + "source": [ + "# Setup" + ] + }, + { + "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 pandas as pd\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "eSHpLxdt1jmE" + }, + "outputs": [], + "source": [ + "import PIL\n", + "from flatland.utils.rendertools import RenderTool\n", + "import imageio\n", + "import os" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "PU5GkH271guD" + }, + "outputs": [ + { + "data": { + "text/html": [ + "<style>.container { width:95% !important; }</style>" + ], + "text/plain": [ + "<IPython.core.display.HTML object>" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from IPython.display import clear_output\n", + "from IPython.core import display\n", + "import ipywidgets as ipw\n", + "display.display(display.HTML(\"<style>.container { width:95% !important; }</style>\"))" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "PU5GkH271guD" + }, + "outputs": [], + "source": [ + "from flatland.envs.rail_env import RailEnv\n", + "from flatland.envs.rail_generators import sparse_rail_generator\n", + "from flatland.envs.schedule_generators import sparse_schedule_generator\n", + "from flatland.envs.malfunction_generators import malfunction_from_file, no_malfunction_generator\n", + "from flatland.envs.rail_generators import rail_from_file\n", + "from flatland.envs.schedule_generators import schedule_from_file\n", + "from flatland.core.env_observation_builder import DummyObservationBuilder\n", + "from flatland.envs.persistence import RailEnvPersister\n", + "from flatland.evaluators.client import FlatlandRemoteClient, TimeoutException\n", + "import flatland.evaluators.service as fes" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "PU5GkH271guD" + }, + "outputs": [], + "source": [ + "import pickle\n", + "import redis\n", + "import subprocess as sp\n", + "import shlex\n", + "import time\n", + "import pkg_resources as pr\n", + "import importlib_resources as ir\n", + "import sys, os\n", + "import pandas as pd" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "/home/jeremy/projects/aicrowd/rl-trains/flatland5/notebooks\r\n" + ] + } + ], + "source": [ + "!pwd" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Find the real path of the `env_data` package (should be copied by tox)" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "<class 'pathlib.PosixPath'> /home3/jeremy/projects/aicrowd/rl-trains/flatland5/env_data/tests/test_001.pkl\n" + ] + } + ], + "source": [ + "with ir.path(\"env_data.tests\", \"test_001.pkl\") as oPath:\n", + " sPath = oPath\n", + "print(type(sPath), sPath)" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'/home3/jeremy/projects/aicrowd/rl-trains/flatland5/env_data/tests/service_test/'" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sDirRoot = \"/\" + \"/\".join(sPath.parts[1:-1] + (\"service_test\",\"\"))\n", + "sDirRoot" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Clear any old redis keys" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "oRedis = redis.Redis()" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[b'flatland-rl::FLATLAND_RL_SERVICE_ID::response::9233d209716f4ae78a5dbe124de67e27']" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "lKeys = oRedis.keys(\"flatland*\")\n", + "lKeys" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Deleting: b'flatland-rl::FLATLAND_RL_SERVICE_ID::response::9233d209716f4ae78a5dbe124de67e27'\n" + ] + } + ], + "source": [ + "for sKey in lKeys:\n", + " print(\"Deleting:\", sKey)\n", + " oRedis.delete(sKey)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Remove `/tmp/output.csv`" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [], + "source": [ + "!rm -f /tmp/output.csv" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### kill any old `service.py` process" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [], + "source": [ + "!ps -ef | grep -i python | grep -i flatland.evaluators.service | awk '{print $2}' | xargs kill" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [], + "source": [ + "osEnv2 = os.environ.copy()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Timeouts copied from service.py" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [], + "source": [ + "#MAX_SUCCESSIVE_TIMEOUTS = int(os.getenv(\"FLATLAND_MAX_SUCCESSIVE_TIMEOUTS\", 10))\n", + "\n", + "# 8 hours (will get debug timeout from env variable if applicable)\n", + "#OVERALL_TIMEOUT = int(os.getenv(\n", + "# \"FLATLAND_OVERALL_TIMEOUT\",\n", + "# 8 * 60 * 60))\n", + "\n", + "# 10 mins\n", + "#INTIAL_PLANNING_TIMEOUT = int(os.getenv(\n", + "# \"FLATLAND_INITIAL_PLANNING_TIMEOUT\",\n", + "# 10 * 60))\n", + "\n", + "# 10 seconds\n", + "#PER_STEP_TIMEOUT = int(os.getenv(\n", + "# \"FLATLAND_PER_STEP_TIMEOUT\",\n", + "# 10))\n", + "\n", + "# 5 min - applies to the rest of the commands\n", + "#DEFAULT_COMMAND_TIMEOUT = int(os.getenv(\n", + "# \"FLATLAND_DEFAULT_COMMAND_TIMEOUT\",\n", + "# 5 * 60))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Set some short timeouts for testing" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [], + "source": [ + "osEnv2[\"FLATLAND_OVERALL_TIMEOUT\"]=\"10\"\n", + "osEnv2[\"FLATLAND_PER_STEP_TIMEOUT\"] = \"2\"\n", + "osEnv2[\"FLATLAND_MAX_SUCCESSIVE_TIMEOUTS\"] = \"2\"" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Create the python command for `service.py`" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "python -m flatland.evaluators.service --test_folder /home3/jeremy/projects/aicrowd/rl-trains/flatland5/env_data/tests/service_test/ --pickle\n", + "['python', '-m', 'flatland.evaluators.service', '--test_folder', '/home3/jeremy/projects/aicrowd/rl-trains/flatland5/env_data/tests/service_test/', '--pickle']\n" + ] + } + ], + "source": [ + "#sCmd = \"python -m flatland.evaluators.service --test_folder ../env_data/tests/service_test --mergeDir ./tmp/merge --actionDir ./tmp/actions --pickle --missingOnly\"\n", + "#sCmd = \"python -m flatland.evaluators.service --test_folder ../env_data/tests/service_test --pickle\" # --verbose\"\n", + "sCmd = f\"python -m flatland.evaluators.service --test_folder {sDirRoot} --pickle\" # --verbose\"\n", + "lsCmd = shlex.split(sCmd)\n", + "print(sCmd)\n", + "print(lsCmd)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Run the command with Popen (output goes to jupyter stdout not notebook)" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [], + "source": [ + "oPipe = sp.Popen(lsCmd, env=osEnv2)" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [], + "source": [ + "oPipe.poll()" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [], + "source": [ + "#oFRC = FlatlandRemoteClient(test_envs_root=\"../env_data/tests/service_test/\", verbose=False, use_pickle=True)\n", + "oFRC = FlatlandRemoteClient(test_envs_root=sDirRoot, verbose=False, use_pickle=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [], + "source": [ + "#env, env_dict = RailEnvPersister.load_new(\"../env_data/tests/service_test/Test_0/Level_0.pkl\") # env_file)\n", + "env, env_dict = RailEnvPersister.load_new(f\"{sDirRoot}/Test_0/Level_0.pkl\") # env_file)\n", + "ldActions = env_dict[\"actions\"]" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [], + "source": [ + "def expert_controller(obs, _env):\n", + " return ldActions[_env._elapsed_steps]\n", + "\n", + "def random_controller(obs, _env):\n", + " dAct = {}\n", + " for iAg in range(len(_env.agents)):\n", + " dAct[iAg] = np.random.randint(0, 5)\n", + " return dAct" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": {}, + "outputs": [], + "source": [ + "oObsB = DummyObservationBuilder()" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 25, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "oObsB.get()" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": {}, + "outputs": [], + "source": [ + "def run_submission(slow_ep=1, delay=2):\n", + " episode = 0\n", + " obs = True\n", + " while obs:\n", + " obs, info = oFRC.env_create(obs_builder_object=oObsB)\n", + " if not obs:\n", + " print(\"null observation - all envs completed!\")\n", + " break\n", + " print(f\"Episode : {episode}\")\n", + " \n", + "\n", + " print(oFRC.env.dones['__all__'])\n", + "\n", + " while True:\n", + " if episode < 3:\n", + " action = expert_controller(obs, oFRC.env)\n", + " else:\n", + " action = random_controller(obs, oFRC.env)\n", + " \n", + " time_start = time.time()\n", + " \n", + " if (episode == slow_ep) and (oFRC.env._elapsed_steps > 10):\n", + " time.sleep(2)\n", + " \n", + " try:\n", + " observation, all_rewards, done, info = oFRC.env_step(action)\n", + " time_diff = time.time() - time_start\n", + " print(\".\", end=\"\")\n", + " if done['__all__']:\n", + " print(\"\\nCompleted Episode : \", episode)\n", + " print(\"Reward : \", sum(list(all_rewards.values())))\n", + " break\n", + " except TimeoutException as err:\n", + " print(\"Timeout: \", err)\n", + " break\n", + " \n", + " episode += 1\n", + " \n", + " print(f\"Evaluation Complete - episodes={episode} - send submit message...\")\n", + " print(oFRC.submit())\n", + " print(\"All done.\")\n", + " " + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "metadata": { + "scrolled": false + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "DEPRECATED - use FileMalfunctionGen instead of malfunction_from_file\n", + "DEPRECATED - RailEnv arg: malfunction_and_process_data - use malfunction_generator\n", + "Episode : 0\n", + "False\n", + "...........................................................................................................................................................................\n", + "Completed Episode : 0\n", + "Reward : 10.0\n", + "DEPRECATED - use FileMalfunctionGen instead of malfunction_from_file\n", + "DEPRECATED - RailEnv arg: malfunction_and_process_data - use malfunction_generator\n", + "Episode : 1\n", + "False\n", + "...........Error received: {'type': 'FLATLAND_RL.ENV_STEP_TIMEOUT'}\n", + "Timeout: FLATLAND_RL.ENV_STEP_TIMEOUT\n", + "null observation - all envs completed!\n", + "Evaluation Complete - episodes=2 - send submit message...\n", + "====================================================================================================\n", + "====================================================================================================\n", + "## Client Performance Stats\n", + "====================================================================================================\n", + "\t - env_creation_wait_time\t => min: 0.0015025138854980469 || mean: 0.004898786544799805 || max: 0.008767366409301758\n", + "\t - internal_env_reset_time\t => min: 0.0014307498931884766 || mean: 0.0017570257186889648 || max: 0.002083301544189453\n", + "\t - inference_time(approx)\t => min: 2.2172927856445312e-05 || mean: 0.010976547751921773 || max: 2.0020840167999268\n", + "\t - internal_env_step_time\t => min: 0.0003039836883544922 || mean: 0.0007923906976050074 || max: 0.0018360614776611328\n", + "====================================================================================================\n", + "{'mean_reward': 1978.0, 'mean_normalized_reward': 0.40367, 'mean_percentage_complete': 0.5}\n", + "All done.\n" + ] + } + ], + "source": [ + "try:\n", + " run_submission()\n", + "except TimeoutException as timeoutException:\n", + " print(\"Timed out.\")\n", + " print(timeoutException)\n", + " " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Kill the evaluator process we started earlier" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": {}, + "outputs": [], + "source": [ + "!ps -ef | grep -i python | grep -i flatland.evaluators.service | awk '{print $2}' | xargs kill" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "<div>\n", + "<style scoped>\n", + " .dataframe tbody tr th:only-of-type {\n", + " vertical-align: middle;\n", + " }\n", + "\n", + " .dataframe tbody tr th {\n", + " vertical-align: top;\n", + " }\n", + "\n", + " .dataframe thead th {\n", + " text-align: right;\n", + " }\n", + "</style>\n", + "<table border=\"1\" class=\"dataframe\">\n", + " <thead>\n", + " <tr style=\"text-align: right;\">\n", + " <th></th>\n", + " <th>0</th>\n", + " <th>1</th>\n", + " </tr>\n", + " </thead>\n", + " <tbody>\n", + " <tr>\n", + " <th>filename</th>\n", + " <td>Test_0/Level_0.pkl</td>\n", + " <td>Test_0/Level_1.pkl</td>\n", + " </tr>\n", + " <tr>\n", + " <th>test_id</th>\n", + " <td>Test_0</td>\n", + " <td>Test_0</td>\n", + " </tr>\n", + " <tr>\n", + " <th>env_id</th>\n", + " <td>Level_0</td>\n", + " <td>Level_1</td>\n", + " </tr>\n", + " <tr>\n", + " <th>n_agents</th>\n", + " <td>5</td>\n", + " <td>5</td>\n", + " </tr>\n", + " <tr>\n", + " <th>x_dim</th>\n", + " <td>25</td>\n", + " <td>25</td>\n", + " </tr>\n", + " <tr>\n", + " <th>y_dim</th>\n", + " <td>25</td>\n", + " <td>25</td>\n", + " </tr>\n", + " <tr>\n", + " <th>n_cities</th>\n", + " <td>2</td>\n", + " <td>2</td>\n", + " </tr>\n", + " <tr>\n", + " <th>max_rails_in_city</th>\n", + " <td>3</td>\n", + " <td>3</td>\n", + " </tr>\n", + " <tr>\n", + " <th>malfunction_interval</th>\n", + " <td>50</td>\n", + " <td>50</td>\n", + " </tr>\n", + " <tr>\n", + " <th>n_envs_run</th>\n", + " <td>50</td>\n", + " <td>50</td>\n", + " </tr>\n", + " <tr>\n", + " <th>seed</th>\n", + " <td>11111</td>\n", + " <td>11111</td>\n", + " </tr>\n", + " <tr>\n", + " <th>grid_mode</th>\n", + " <td>False</td>\n", + " <td>False</td>\n", + " </tr>\n", + " <tr>\n", + " <th>max_rails_between_cities</th>\n", + " <td>2</td>\n", + " <td>2</td>\n", + " </tr>\n", + " <tr>\n", + " <th>malfunction_duration_min</th>\n", + " <td>20</td>\n", + " <td>20</td>\n", + " </tr>\n", + " <tr>\n", + " <th>malfunction_duration_max</th>\n", + " <td>50</td>\n", + " <td>50</td>\n", + " </tr>\n", + " <tr>\n", + " <th>speed_ratios</th>\n", + " <td>{1.0: 1.0}</td>\n", + " <td>{1.0: 1.0}</td>\n", + " </tr>\n", + " <tr>\n", + " <th>reward</th>\n", + " <td>-944</td>\n", + " <td>4900</td>\n", + " </tr>\n", + " <tr>\n", + " <th>normalized_reward</th>\n", + " <td>0.807347</td>\n", + " <td>0</td>\n", + " </tr>\n", + " <tr>\n", + " <th>percentage_complete</th>\n", + " <td>1</td>\n", + " <td>0</td>\n", + " </tr>\n", + " <tr>\n", + " <th>steps</th>\n", + " <td>171</td>\n", + " <td>12</td>\n", + " </tr>\n", + " <tr>\n", + " <th>simulation_time</th>\n", + " <td>0.189325</td>\n", + " <td>2.03193</td>\n", + " </tr>\n", + " <tr>\n", + " <th>nb_malfunctioning_trains</th>\n", + " <td>2</td>\n", + " <td>0</td>\n", + " </tr>\n", + " <tr>\n", + " <th>nb_deadlocked_trains</th>\n", + " <td>0</td>\n", + " <td>0</td>\n", + " </tr>\n", + " <tr>\n", + " <th>controller_inference_time_min</th>\n", + " <td>2.21729e-05</td>\n", + " <td>2.21729e-05</td>\n", + " </tr>\n", + " <tr>\n", + " <th>controller_inference_time_mean</th>\n", + " <td>3.48147e-05</td>\n", + " <td>3.63968e-05</td>\n", + " </tr>\n", + " <tr>\n", + " <th>controller_inference_time_max</th>\n", + " <td>0.000115633</td>\n", + " <td>0.000169754</td>\n", + " </tr>\n", + " </tbody>\n", + "</table>\n", + "</div>" + ], + "text/plain": [ + " 0 1\n", + "filename Test_0/Level_0.pkl Test_0/Level_1.pkl\n", + "test_id Test_0 Test_0\n", + "env_id Level_0 Level_1\n", + "n_agents 5 5\n", + "x_dim 25 25\n", + "y_dim 25 25\n", + "n_cities 2 2\n", + "max_rails_in_city 3 3\n", + "malfunction_interval 50 50\n", + "n_envs_run 50 50\n", + "seed 11111 11111\n", + "grid_mode False False\n", + "max_rails_between_cities 2 2\n", + "malfunction_duration_min 20 20\n", + "malfunction_duration_max 50 50\n", + "speed_ratios {1.0: 1.0} {1.0: 1.0}\n", + "reward -944 4900\n", + "normalized_reward 0.807347 0\n", + "percentage_complete 1 0\n", + "steps 171 12\n", + "simulation_time 0.189325 2.03193\n", + "nb_malfunctioning_trains 2 0\n", + "nb_deadlocked_trains 0 0\n", + "controller_inference_time_min 2.21729e-05 2.21729e-05\n", + "controller_inference_time_mean 3.48147e-05 3.63968e-05\n", + "controller_inference_time_max 0.000115633 0.000169754" + ] + }, + "execution_count": 29, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "pd.read_csv(\"/tmp/output.csv\").T" + ] + } + ], + "metadata": { + "colab": { + "collapsed_sections": [], + "name": "Flatland Round 2 Replays", + "provenance": [] + }, + "hide_input": false, + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "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 + }, + "widgets": { + "application/vnd.jupyter.widget-state+json": { + "state": { + "0dd673bfc308419c8f62c545999562b3": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "1bc1201efe3e4e3a8403e4b8c902a295": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "26afede661e541db9d09f4bd88895c7b": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "FloatSliderModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "FloatSliderModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "FloatSliderView", + "continuous_update": true, + "description": "frame_idx", + "description_tooltip": null, + "disabled": false, + "layout": "IPY_MODEL_e042a431167b452a9e9f2f0a0ac99f45", + "max": 29, + "min": 0, + "orientation": "horizontal", + "readout": true, + "readout_format": ".2f", + "step": 1, + "style": "IPY_MODEL_40b60736128543f48f32eb1f7c89d855", + "value": 0 + } + }, + "40b60736128543f48f32eb1f7c89d855": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "SliderStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "SliderStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "", + "handle_color": null + } + }, + "4a12b47571a0481b881e564bbbcf6f53": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "VBoxModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "VBoxModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "VBoxView", + "box_style": "", + "children": [ + "IPY_MODEL_78910a9b607e4a47b06b5c2cf03811a7" + ], + "layout": "IPY_MODEL_1bc1201efe3e4e3a8403e4b8c902a295" + } + }, + "55f6067b15be4de4b9ab165d4ff7009b": { + "model_module": "@jupyter-widgets/output", + "model_module_version": "1.0.0", + "model_name": "OutputModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/output", + "_model_module_version": "1.0.0", + "_model_name": "OutputModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/output", + "_view_module_version": "1.0.0", + "_view_name": "OutputView", + "layout": "IPY_MODEL_f8b98bf694c848baa97f2ef4e9e599db", + "msg_id": "", + "outputs": [] + } + }, + "78910a9b607e4a47b06b5c2cf03811a7": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "PlayModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "PlayModel", + "_playing": false, + "_repeat": false, + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "PlayView", + "description": "", + "description_tooltip": null, + "disabled": false, + "interval": 250, + "layout": "IPY_MODEL_a8de6f99082e428dae860e4c6a79b9cc", + "max": 29, + "min": 0, + "show_repeat": true, + "step": 1, + "style": "IPY_MODEL_0dd673bfc308419c8f62c545999562b3", + "value": 0 + } + }, + "86c96853eb074ec18c60567cd4e8b134": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "VBoxModel", + "state": { + "_dom_classes": [ + "widget-interact" + ], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "VBoxModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "VBoxView", + "box_style": "", + "children": [ + "IPY_MODEL_26afede661e541db9d09f4bd88895c7b", + "IPY_MODEL_55f6067b15be4de4b9ab165d4ff7009b" + ], + "layout": "IPY_MODEL_bb522116f06a4f1babe2a3c0c557654d" + } + }, + "a8de6f99082e428dae860e4c6a79b9cc": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "bb522116f06a4f1babe2a3c0c557654d": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "e042a431167b452a9e9f2f0a0ac99f45": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "f8b98bf694c848baa97f2ef4e9e599db": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + } + }, + "version_major": 2, + "version_minor": 0 + } + } + }, + "nbformat": 4, + "nbformat_minor": 1 +}