Scenario Generation#
Rail Generators, Line Generators and Timetable Generators#
The separation between rail generation and schedule generation reflects the organisational separation in the railway domain
Infrastructure Manager (IM): is responsible for the layout and maintenance of tracks simulated by
rail_generator
.Railway Undertaking (RU): operates trains on the infrastructure Usually, there is a third organisation, which ensures discrimination-free access to the infrastructure for concurrent requests for the infrastructure in a schedule planning phase simulated by
line_generator
andtimetable_generator
. However, in the Flatland challenge, we focus on the re-scheduling problem during live operations.
We can produce RailGenerator
s by completing the following:
from typing import Mapping, List
from numpy.random.mtrand import RandomState
from flatland.envs.line_generators import LineGenerator
from flatland.envs.distance_map import DistanceMap
from flatland.envs.agent_utils import EnvAgent
from flatland.envs.timetable_utils import Timetable
from flatland.envs.rail_env import RailEnv
from flatland.envs import rail_generators as rail_gen
from flatland.envs import line_generators as line_gen
import flatland.envs.timetable_generators as ttg
from flatland.utils import seeding
def sparse_rail_generator(max_num_cities=5, grid_mode=False, max_rails_between_cities=4,
max_rail_pairs_in_city=4, seed=0):
def generator(width, height, num_agents, num_resets=0):
# generate the grid and (optionally) some hints for the line_generator
...
return grid_map, {'agents_hints': {
'num_agents': num_agents,
'city_positions': city_positions,
'train_stations': train_stations,
'city_orientations': city_orientations
}}
return generator
Similarly, LineGenerator
s:
def sparse_line_generator(speed_ratio_map: Mapping[float, float] = None) -> LineGenerator:
def generator(rail: GridTransitionMap, num_agents: int, hints: Any = None):
# place agents:
# - initial position
# - initial direction
# - targets
# - speed data
# - malfunction data
...
return agents_position, agents_direction, agents_target, speeds, agents_malfunction
return generator
And finally, timetable_generator
is called within the RailEnv
βs reset() during line generation to create a time table for the trains.
def timetable_generator(agents: List[EnvAgent], distance_map: DistanceMap,
agents_hints: dict, np_random: RandomState = None) -> Timetable:
# specify:
# - earliest departures
# - latest arrivals
# - max episode steps
...
return Timetable(earliest_departures, latest_arrivals, max_episode_steps)
env = RailEnv(
width=30,
height=30,
rail_generator=rail_gen.sparse_rail_generator(
max_num_cities=2,
seed=42,
grid_mode=False,
max_rails_between_cities=2,
max_rail_pairs_in_city=2
),
line_generator=line_gen.sparse_line_generator(speed_ratio_map={1.0: 0.25, 0.5: 0.25, 0.33: 0.25, 0.25: 0.25}, seed=42),
timetable_generator=ttg.timetable_generator,
)
obs, info = env.reset()
info
{'action_required': {0: False, 1: False},
'malfunction': {0: 0, 1: 0},
'speed': {0: 0.3333333333333333, 1: 0.3333333333333333},
'state': {0: <TrainState.WAITING: 0>, 1: <TrainState.WAITING: 0>}}
Inside reset()
, rail, line and timetable generators are called as follows:
rail, optionals = rail_gen.sparse_rail_generator(
max_num_cities=2,
seed=42,
grid_mode=False,
max_rails_between_cities=2,
max_rail_pairs_in_city=2
)(30,30,2)
optionals
{'agents_hints': {'city_positions': [(10, 7), (24, 12)],
'train_stations': [[((10, 7), 0), ((10, 8), 1)],
[((24, 12), 0), ((24, 13), 1)]],
'city_orientations': [<Grid4TransitionsEnum.SOUTH: 2>,
<Grid4TransitionsEnum.NORTH: 0>]},
'level_free_positions': set()}
line_gen.sparse_line_generator(speed_ratio_map={1.0: 0.25, 0.5: 0.25, 0.33: 0.25, 0.25: 0.25}, seed=42)(rail, 2, optionals["agents_hints"], np_random=seeding.np_random(42)[0])
Line(agent_positions=[[(10, 7)], [(24, 12)]], agent_directions=[[<Grid4TransitionsEnum.NORTH: 0>], [<Grid4TransitionsEnum.NORTH: 0>]], agent_targets=[(24, 13), (10, 8)], agent_speeds=[0.33, 1.0])
Notice that the rail_generator
may pass agents_hints
to the line_generator
and timetable_generator
which the latter may interpret.
For instance, the way the sparse_rail_generator
generates the grid, it already determines the agentβs goal and target.
Hence, rail_generator
, line_generator
and timetable_generator
have to match if line_generator
presupposes some specific agents_hints
.
Currently, the only one used are the sparse_rail_generator
, sparse_line_generator
and the timetable_generator
which works in conjunction with these.
Rail Generator#
Available Rail Generators#
Flatland provides the sparse_rail_generator
, which generates realistic-looking railway networks.
Sparse rail generator#
The idea behind the sparse rail generator is to mimic classic railway structures where dense nodes (cities) are sparsely connected to each other and where you have to manage traffic flow between the nodes efficiently. The cities in this level generator are much simplified in comparison to real city networks but they mimic parts of the problems faced in daily operations of any railway company.
There are a number of parameters you can tune to build your own map and test different complexity levels of the levels.
Note
Some combinations of parameters do not go well together and will lead to infeasible level generation. In the worst case, the level generator will issue a warning when it cannot build the environment according to the parameters provided.
You can see that you now need both a rail_generator
and a line_generator
to generate a level. These need to work nicely together. The rail_generator
will generate the railway infrastructure and provide hints to the line_generator
about where to place agents. The line_generator
will then generate a Line by placing agents at different train stations and providing them with individual targets.
You can tune the following parameters in the sparse_rail_generator
:
max_num_cities
: Maximum number of cities to build. The generator tries to achieve this numbers given all the other parameters. Cities are the only nodes that can host start and end points for agent tasks (train stations).grid_mode
: How to distribute the cities in the path, either equally in a grid or randomly.max_rails_between_cities
: Maximum number of rails connecting cities. This is only the number of connection points at city border. The number of tracks drawn in-between cities can still vary.max_rails_in_city
: Maximum number of parallel tracks inside the city. This represents the number of tracks in the train stations.seed
: The random seed used to initialize the random generator. Can be used to generate reproducible networks.
π’ Over- and underpasses (aka. level-free diamond crossings)#
This feature was introduced in 4.0.5
Description#
Introduce level-free crossings. This reflects core railway domain features.
In particular, Diamond crossing can be defined to be level-free, which allows two trains to occupy the cell if one runs horizontal and the other vertical.
Implementation#
SparseRailGen
has a new option
p_level_free : float
Percentage of diamond-crossings which are level-free.
RailEnv
keeps tracks of level-free diamond crossings:
self.level_free_positions: Set[Vector2D] = set()
The RailEnv
will then allow two agents to be in the same cell concurrently if one is running horizontally and the other is running vertically.
Line Generator#
π Speed profiles (aka. Multi-Speed)#
This feature was introduced in 3.0.0
Finally, trains in real railway networks donβt all move at the same speed. A freight train will for example be slower than a passenger train. This is an important consideration, as you want to avoid scheduling a fast train behind a slow train!
Agents can have speed profiles, reflecting different train classes (passenger, freight, etc.).
One of the main contributions to the complexity of railway network operations stems from the fact that all trains travel at different speeds while sharing a very limited railway network. In Flatland 3 this feature will be enabled as well and will lead to much more complex configurations. Here we count on your support if you find bugs or improvements :).
The different speed profiles can be generated using the schedule_generator
, where you can actually chose as many different speeds as you like.
Keep in mind that the fastest speed is 1 and all slower speeds must be between 1 and 0.
For the submission scoring you can assume that there will be no more than 5 speed profiles.
Later versions of Flatland might have varying speeds during episodes. Therefore, we return the agent speeds.
Notice that we do not guarantee that the speed will be computed at each step, but if not costly we will return it at each step.
In your controller, you can get the agentsβ speed from the info
returned by step
:
obs, rew, done, info = env.step(actions)
...
for a in range(env.get_num_agents()):
speed = info['speed'][a]
import inspect
from flatland.envs.agent_utils import EnvAgent
from flatland.envs.rail_env import RailEnv
from flatland.envs.rail_env_action import RailEnvActions
from flatland.env_generation.env_generator import env_generator
env, _, _ = env_generator()
for _ in range(25):
obs, rew, done, info = env.step({i: RailEnvActions.MOVE_FORWARD for i in env.get_agent_handles()})
print("after 25 steps")
for a in range(env.get_num_agents()):
speed = info['speed'][a]
print(f"\tagent {a} has speed {speed} in state {env.agents[a].state.name}")
print("after 26 steps")
obs, rew, done, info = env.step({i: RailEnvActions.STOP_MOVING for i in env.get_agent_handles()})
for a in range(env.get_num_agents()):
speed = info['speed'][a]
print(f"\tagent {a} has speed {speed} in state {env.agents[a].state.name}")
/opt/hostedtoolcache/Python/3.10.17/x64/lib/python3.10/site-packages/flatland/envs/rail_generators.py:321: UserWarning: Could not set all required cities! Created 1/2
warnings.warn(city_warning)
/opt/hostedtoolcache/Python/3.10.17/x64/lib/python3.10/site-packages/flatland/envs/rail_generators.py:217: UserWarning: [WARNING] Changing to Grid mode to place at least 2 cities.
warnings.warn("[WARNING] Changing to Grid mode to place at least 2 cities.")
after 25 steps
agent 0 has speed 0.25 in state MOVING
agent 1 has speed 1.0 in state WAITING
agent 2 has speed 0.25 in state MOVING
agent 3 has speed 1.0 in state WAITING
agent 4 has speed 1.0 in state WAITING
agent 5 has speed 0.25 in state WAITING
agent 6 has speed 0.5 in state WAITING
after 26 steps
agent 0 has speed 0 in state STOPPED
agent 1 has speed 1.0 in state WAITING
agent 2 has speed 0 in state STOPPED
agent 3 has speed 1.0 in state WAITING
agent 4 has speed 1.0 in state WAITING
agent 5 has speed 0.25 in state WAITING
agent 6 has speed 0.5 in state WAITING
π Multi-stop Schedules (w/o alternatives/routing flexibility)#
This feature was introduced in 4.0.5
Description#
Introduce intermediate targets in schedule and reward function. This reflects core railway domain features.
In particular, Flatland timetable can have several intermediate targets with time window earliest, latest. (Negative) rewards for not serving intermediate targets or not respecting earliest/latest window can be configured. Schedule generator can be configured with number of intermediate targets.
Implementation#
flatland.envs.line_generators.SparseLineGen
takes an additional option
line_length : int
The length of the lines. Defaults to 2.
A Line
now allows for multiple intermediate positions/directions and a Timetable
contains a time window for each stop:
import inspect
import flatland.envs.timetable_utils
print("".join(inspect.getsourcelines(flatland.envs.timetable_utils)[0]))
from typing import List, NamedTuple
from flatland.core.grid.grid4 import Grid4TransitionsEnum
from flatland.core.grid.grid_utils import IntVector2DArray, IntVector2DArrayArray
Line = NamedTuple('Line', [
# positions and directions without target (which has no direction)
('agent_positions', IntVector2DArrayArray),
('agent_directions', List[List[Grid4TransitionsEnum]]),
('agent_targets', IntVector2DArray),
('agent_speeds', List[float]),
])
Timetable = NamedTuple('Timetable', [
# earliest departures and latest arrivals including None for latest arrival at initial and None for earliest departure at target
('earliest_departures', List[List[int]]),
('latest_arrivals', List[List[int]]),
('max_episode_steps', int)
])
In addition, Rewards
introduces 3 new penalties for intermediate stops:
- intermediate_not_served_penalty = -1
- intermediate_late_arrival_penalty_factor = 0.2
- intermediate_early_departure_penalty_factor = 0.5
Note that earliest_departure
at the initial position is enforced by the RailEnv
(i.e. an agent cannot start before that timestep) whereas the time windows for intermediate stops are not enforced by the RailEnv
but penalized only by the Rewards
configuration.
Timetable Generator#
This feature was introduced in
flatland 3.0.0
Background#
Up until this point, the trains in Flatland were allowed to depart and arrive whenever they desired, the only goal was to make every train reach its destination as fast as possible. However, things are quite different in the real world. Timing and punctuality are crucial to railways. Trains have specific schedules. They are expected to depart and arrive at particular times.
This concept has been introduced to the environment in Flatland 3.0. Trains now have a time window within which they are expected to start and reach their destination.