Scenarios generate worlds#
In this tutorial we take a closer look at how scenarios generate world.
Groups#
The YAML description of a scenario lists groups of agents. For example,
[1]:
from navground import sim
scenario = sim.load_scenario("""
groups:
- number: 1
radius: 1
""")
describes a single group composed of one agent of radius 1.
scenario.groups
[2]:
group = scenario.groups[0]
Groups are generators of agents. Given a world, sample
generates an agent with the specifics of the group
[3]:
world = sim.World()
agent = group.sample(world)
print(agent.radius)
1.0
and add_to_world
sample and add a number
of agents from the group
[4]:
group.add_to_world(world)
print([a.radius for a in world.agents])
[1.0]
We generally do not use groups direcly like this. Instead, we let the scenario generate a world using its groups
[5]:
world = scenario.make_world()
which will contains a single agent of radius 1 in our case
[6]:
print([a.radius for a in world.agents])
[1.0]
Groups can be loaded from YAML and added to the scenario manually. Let us add a second group with one larger agent.
[7]:
group = sim.AgentSampler.load("""
number: 1
radius: 10
""")
scenario.add_group(group)
[8]:
world = scenario.make_world()
print([a.radius for a in world.agents])
[1.0, 10.0]
Everything is a sampler#
In a group, almost every attribute corresponds to a sampler. In the YAML, specifying radius of 1.0, does actually creates a sampler (for radius value) that generate 1 everytime.
Let us be more specific and define a scenario with an explicit sampler.
[9]:
scenario = sim.load_scenario("""
groups:
- number: 1
radius:
sampler: constant
value: 1
""")
[10]:
world = scenario.make_world()
world.agents[0].radius
[10]:
1.0
Let us pick a more interesting sampler. For instance, we may want the radii of the agent to be uniformly distributed in [0.5, 1.5] instead:
[11]:
scenario = sim.load_scenario("""
groups:
- number: 1
radius:
sampler: uniform
from: 0.5
to: 1.5
""")
It is very important that experiments are reproducible. Therefore, samplers should be generate reproducible values too, which does happen if we speficy the same seed.
[12]:
for _ in range(5):
world = scenario.make_world(seed=1)
print(world.agents[0].radius)
0.9170219898223877
0.9170219898223877
0.9170219898223877
0.9170219898223877
0.9170219898223877
Sharing the sampler within the group#
Let us more agents to the group.
[13]:
scenario = sim.load_scenario("""
groups:
- number: 5
radius:
sampler: uniform
from: 0.5
to: 1.5
""")
The generated worlds will contains two agents, with radii sampled from the same sampler in sequence:
[14]:
world = scenario.make_world(seed=1)
print([agent.radius for agent in world.agents])
[0.9170219898223877, 1.4971847534179688, 1.2203245162963867, 1.4325573444366455, 0.500114381313324]
We may want instead that the radii are still sampled uniformly from [0.5, 1.5] by shared by all agents in the same world. In this case, we specify that the sampled should sample only once
per group.
[15]:
scenario = sim.load_scenario("""
groups:
- number: 5
radius:
sampler: uniform
from: 0.5
to: 1.5
once: true
""")
[16]:
world = scenario.make_world(seed=1)
print([agent.radius for agent in world.agents])
[0.9170219898223877, 0.9170219898223877, 0.9170219898223877, 0.9170219898223877, 0.9170219898223877]
A different seed will still generate different values:
[17]:
world = scenario.make_world(seed=2)
print([agent.radius for agent in world.agents])
[0.9359948635101318, 0.9359948635101318, 0.9359948635101318, 0.9359948635101318, 0.9359948635101318]
Deterministic samplers#
The are two families of samplers in navground.
Deterministic samplers use the seed as an index in a pre-determined sequence. For example, to generate a worlds where agents have an increasingly large radius of 1, 1.5, 2, 2.5, ...
, we specify a regular
sampler
[18]:
scenario = sim.load_scenario("""
groups:
- number: 3
radius:
sampler: regular
from: 1.0
step: 0.5
once: true
""")
[19]:
for seed in range(5):
world = scenario.make_world(seed=seed)
print([agent.radius for agent in world.agents])
[1.0, 1.0, 1.0]
[1.5, 1.5, 1.5]
[2.0, 2.0, 2.0]
[2.5, 2.5, 2.5]
[3.0, 3.0, 3.0]
Deterministic samplers treat the seed differently depending if the are shared or not within the group.
Let us verify what happens if the same sampler is not shared between the three agents
[20]:
scenario = sim.load_scenario("""
groups:
- number: 3
radius:
sampler: regular
from: 1.0
step: 0.5
once: false
""")
[21]:
for seed in range(5):
world = scenario.make_world(seed=seed)
print([agent.radius for agent in world.agents])
[1.0, 1.5, 2.0]
[1.0, 1.5, 2.0]
[1.0, 1.5, 2.0]
[1.0, 1.5, 2.0]
[1.0, 1.5, 2.0]
In this case, the agents have increasingly large radii across the group but the same values are repeated in different worlds.
Pseudo-random samplers#
Random samplers, like uniform
, are a bit different. They sample a-priori unknown sequences, yet are completely determined by the seed. Therefore they are not repeated in worlds with different seeds. Let us pick another random sampler (a normal distribution) and verify
[22]:
scenario = sim.load_scenario("""
groups:
- number: 3
radius:
sampler: normal
mean: 1.0
std_dev: 0.5
once: false
""")
[23]:
for seed in range(5):
world = scenario.make_world(seed=seed)
print([agent.radius for agent in world.agents])
[1.5815393924713135, 2.1061031818389893, 1.2419023513793945]
[1.387001872062683, 1.0780328512191772, 1.1531997919082642]
[0.8675759434700012, 0.34844720363616943, 1.0356043577194214]
[1.3209900856018066, 1.0448338985443115, 0.6211239099502563]
[1.0918337106704712, 0.36362123489379883, 1.7619856595993042]
Components sampler#
Component associated to an agent (i.e., behaviors, kinematics, behavior modulations, state estimations and tasks) are also sampled when generating a world. For instance, let us could vary the kinematics maximal speed
[24]:
scenario = sim.load_scenario("""
groups:
- number: 3
radius: 1
kinematics:
type: Omni
max_speed:
sampler: uniform
from: 1
to: 2
state_estimation:
type: Bounded
range:
sampler: normal
mean: 2.0
std_dev: 1.0
min: 0.785
max: 3.142
""")
[25]:
world = scenario.make_world(seed=0)
print('Kinematics:\n\n' + world.agents[0].kinematics.dump())
Kinematics:
type: Omni
max_speed: 1.71518934
max_angular_speed: .inf
[26]:
print('State Estimation:\n\n' + world.agents[0].state_estimation.dump())
State Estimation:
type: Bounded
range: 3.14199996
update_static_obstacles: false
The only attribute that always constant (i.e., cannot be sampled), is type
as it defines properties to be loaded from the YAML. To components of different type
, we can define more groups. For example, to alternate between worlds where two agents use either Omni
or 2WDiff
kinematics, we setup two groups:
[27]:
scenario = sim.load_scenario("""
groups:
- number: [2, 0]
radius: 1
kinematics:
type: Omni
- number: [0, 2]
radius: 1
kinematics:
type: 2WDiff
axis: 2
""")
When the first group has 0 agents, the second will have 2 agents and viceversa:
[28]:
for seed in range(5):
world = scenario.make_world(seed=seed)
print([agent.kinematics.type for agent in world.agents])
print('...')
['Omni', 'Omni']
['2WDiff', '2WDiff']
['Omni', 'Omni']
['2WDiff', '2WDiff']
['Omni', 'Omni']
...
Global attributes#
As we have just seen, the number
of agents is also sampled by a sampler. In this case, it is an attribute of the group itself, not of the agent that compose the groups, therefore once
has a different meaning:
once: false
(the default) generates different values depending on the seed
[29]:
scenario = sim.load_scenario("""
groups:
- number:
sampler: uniform
from: 1
to: 10
once: false
""")
for seed in range(5):
world = scenario.make_world(seed=seed)
print(len(world.agents))
6
6
9
9
8
once: true
generates the same value for each world
[30]:
scenario = sim.load_scenario("""
groups:
- number:
sampler: uniform
from: 1
to: 10
once: true
""")
for seed in range(5):
world = scenario.make_world(seed=seed)
print(len(world.agents))
6
6
6
6
6
Scenario properties#
The same applies to scenarios’ properties. Intead of a an empty, vannilla world, let us generate worlds from the corridor scenario:
[31]:
scenario = sim.load_scenario("""
type: Corridor
agent_margin: 0.125
length:
sampler: uniform
from: 5
to: 15
groups:
- number: 2
""")
This scenario defines properties
[32]:
scenario.properties
[32]:
{'add_safety_to_agent_margin': <Property: bool>,
'agent_margin': <Property: float>,
'length': <Property: float>,
'width': <Property: float>}
that are sampled once per world (for once: false
)
[33]:
for seed in range(5):
world = scenario.make_world(seed=seed)
print(scenario.get('length'))
10.48813533782959
9.170219421386719
9.359949111938477
10.507978439331055
14.670297622680664
or once per scenario (for once: true
)
[34]:
scenario = sim.load_scenario("""
type: Corridor
agent_margin: 0.125
length:
sampler: uniform
from: 5
to: 15
once: true
groups:
- number: 2
""")
[35]:
for seed in range(5):
world = scenario.make_world(seed=seed)
print(scenario.get('length'))
10.48813533782959
10.48813533782959
10.48813533782959
10.48813533782959
10.48813533782959
As these properties impact the generation of a whole world, we can store them as world attributes
[36]:
world = scenario.make_world(seed=1)
scenario.set_attributes(world)
print(world.attributes)
{'add_safety_to_agent_margin': True, 'agent_margin': 0.125, 'length': 10.48813533782959, 'width': 1.0}
Extending a scenario#
Specifying samplers through YAML is not always enough. In particular, these sampler are indipendent of each other and cannot therefore generate more complex distribution of worlds.
For instance, let us say that we world like to generate a random number of agents and pair them, so that one is positioned in front of the other (and color them the same, so we keep track of each pair)
We can create a function that implements the pairing
[37]:
from navground import core
from navground.sim.ui import svg_color
def pair_agents(world: sim.World, distance: float) -> None:
rng = world.random_generator
for a, b in zip(world.agents[:-1:2], world.agents[1::2], strict=True):
p = a.position - core.unit(a.orientation) * (a.radius + b.radius + distance)
b.pose = core.Pose2(p, a.orientation )
rbg = world.random_generator.uniform(low=0, high=1, size=3)
a.color = b.color = svg_color(*rbg)
and then apply it to any world
[38]:
from navground.sim.ui import render_default_config
render_default_config.width = 200
scenario = sim.load_scenario("""
bounding_box:
min_x: -2
max_x: 12
min_y: -2
max_y: 12
groups:
- number:
sampler: uniform
from: 10
to: 20
radius: 0.5
position:
sampler: uniform
from: [0, 0]
to: [10, 10]
""")
world = scenario.make_world(seed=0)
world
[38]:
[39]:
pair_agents(world, distance=1.0)
world
[39]:
Add an initializer#
We can let the scenario apply the pairing automatically too, by wrapping it in a world initializer, i.e. a function that takes as input a world and an optional seed
[40]:
def pairing_initializer(distance: float):
def init(world: sim.World, seed: int | None = None) -> None:
pair_agents(world, distance)
return init
scenario.set_init('pair', pairing_initializer(distance=1.0))
[41]:
scenario.make_world(seed=0)
[41]:
As we see, this initialization may give overlapping pairs, which we may avoid by moving pairs apart from each other in the initializer.
Add a (custom) group#
In addition to groups loaded from YAML, we can define groups in Python, subclassing sim.Scenario.Group
. Altough groups are designed to generate and add agents to a world, they can manupulate the world in any way.
In this case, let us define a group that perform the same pairing as the initializer
[42]:
class PairingGroup(sim.Scenario.Group):
def __init__(self, distance: float = 1.0):
super().__init__()
self._distance = distance
def add_to_world(self, world: sim.World, seed: int | None = None):
# Apply sampler defined in YAML
pair_agents(world, distance=self._distance)
add it to the scenario, instead of the initializer,
[43]:
scenario = sim.load_scenario("""
bounding_box:
min_x: -2
max_x: 12
min_y: -2
max_y: 12
groups:
- number:
sampler: uniform
from: 10
to: 20
radius: 0.5
position:
sampler: uniform
from: [0, 0]
to: [10, 10]
""")
scenario.add_group(PairingGroup(distance=1))
[44]:
scenario.groups
[44]:
[<navground.sim._navground_sim.AgentSampler at 0x109d37530>,
<__main__.PairingGroup at 0x109cb7770>]
and get the same results
[45]:
world = scenario.make_world(seed=0)
world
[45]:
Define a custom scenario#
Defining a new scenario that direcly applies the same initializer
[46]:
class PairedScenario(sim.Scenario, name="Paired"):
def __init__(self, distance: float = 1.0):
super().__init__()
self._distance = distance
@property
@sim.register(1.0, 'Distance between agents in a pair')
def distance(self) -> float:
return self._distance
@distance.setter
def distance(self, value) -> None:
self._distance = max(0, value)
def init_world(self, world: sim.World, seed: int | None = None):
# Apply sampler defined in YAML
super().init_world(world, seed=seed)
pair_agents(world, distance=self._distance)
is a more powerfull alternative because it can be initialized from YAML
[47]:
scenario = sim.load_scenario("""
type: Paired
bounding_box:
min_x: -2
max_x: 12
min_y: -2
max_y: 12
groups:
- number:
sampler: uniform
from: 10
to: 20
radius: 0.5
position:
sampler: uniform
from: [0, 0]
to: [10, 10]
""")
[48]:
scenario.make_world(seed=0)
[48]:
Properties like distance
are now automatically assigned samplers too, i.e., we can vary them without any extra code:
[49]:
scenario = sim.load_scenario("""
type: Paired
bounding_box:
min_x: -2
max_x: 12
min_y: -2
max_y: 12
distance:
sampler: uniform
from: 0
to: 2
groups:
- number:
sampler: uniform
from: 10
to: 20
radius: 0.5
position:
sampler: uniform
from: [0, 0]
to: [10, 10]
""")
[50]:
scenario.make_world(seed=2)
[50]:
[51]:
scenario.make_world(seed=3)
[51]:
[52]:
scenario.make_world(seed=4)
[52]:
You find more details about extending scenarios in :doc:../guides/extend/scenario
.