Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix agent-mission combination #1868

Merged
merged 10 commits into from
Feb 21, 2023
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@ Copy and pasting the git commit messages is __NOT__ enough.
### Added
- Added a zoo agent, named Control-and-Supervised-Learning, from NeurIPS 2022 submission. This zoo agent runs in benchmark `driving_smarts==0.0`.
- Added a zoo agent, named Discrete Soft Actor Critic, from NeurIPS 2022 submission. This zoo agent runs in benchmark `driving_smarts==0.0`.
- Added a math utility for generating combination groups out of two with unique index use per group. This is intended for use to generate the combinations needed to give a unique agent-mission set per reset.
### Changed
### Deprecated
### Fixed
- Ensured that `hiwayenv.reset` provides unique agent-mission sets per reset.
### Removed
### Security

Expand Down
58 changes: 7 additions & 51 deletions envision/tests/test_data_formatter.py
Original file line number Diff line number Diff line change
Expand Up @@ -289,60 +289,15 @@ def test_complex_data(complex_data):
def sim_data():
return [
0.1,
None, # scene_id
"straight", # scene_name
None,
"straight",
[
[ # Traffic actor
0, # id
1, # lane id
-1.84, # x
-3.2, # y
0.0, # z
-1.57, # heading
4.62, # speed
None, # events
[], # wapypoint paths
[], # driven path
[], # lidar
[], # geometry
0, # actor type
4, # vehicle type
],
[
2,
1,
-1.84,
0.0,
0.0,
-1.57,
5.0,
None,
[],
[],
[],
[],
0,
4,
],
[
3,
1,
-1.84,
3.2,
0.0,
-1.57,
4.91,
None,
[],
[],
[],
[],
0,
4,
],
[0, 1, -1.84, -3.2, 0.0, -1.57, 5.05, None, [], [], [], [], 0, 4],
[2, 1, -1.84, 0.0, 0.0, -1.57, 5.05, None, [], [], [], [], 0, 4],
[3, 1, -1.84, 3.2, 0.0, -1.57, 4.88, None, [], [], [], [], 0, 4],
],
[],
[[90.0, -10.0, 90.0, 10.0, 110.0, 10.0, 110.0, -10.0, 90.0, -10.0]], # bubbles
[[90.0, -10.0, 90.0, 10.0, 110.0, 10.0, 110.0, -10.0, 90.0, -10.0]],
[],
[4],
{
Expand Down Expand Up @@ -448,6 +403,7 @@ def side_effect(state: State):
es.add_any(state)

data = es.resolve()

assert data == sim_data
assert data == unpack(data)

Expand Down
25 changes: 7 additions & 18 deletions smarts/core/scenario.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@
from smarts.core.traffic_history import TrafficHistory
from smarts.core.utils.file import make_dir_in_smarts_log_dir, path2hash
from smarts.core.utils.id import SocialAgentId
from smarts.core.utils.math import radians_to_vec, vec_to_radians
from smarts.core.utils.math import ordered_combinations, radians_to_vec, vec_to_radians
from smarts.sstudio import types as sstudio_types
from smarts.sstudio.types import MapSpec
from smarts.sstudio.types import Via as SSVia
Expand Down Expand Up @@ -213,7 +213,7 @@ def variations_for_all_scenario_roots(
agent_missions = Scenario.discover_agent_missions(
scenario_root, agents_to_be_briefed
)
agent_missions = [dict(zip(agents_to_be_briefed, agent_missions))]

social_agent_infos = Scenario._discover_social_agents_info(scenario_root)
social_agents = [
{
Expand All @@ -229,20 +229,21 @@ def variations_for_all_scenario_roots(
# `or [None]` so that product(...) will not return an empty result
# but insted a [(..., `None`), ...].
agent_missions = agent_missions or [None]
mission_agent_groups = ordered_combinations(
agents_to_be_briefed, agent_missions
)
social_agents = social_agents or [None]
traffic_histories = Scenario.discover_traffic_histories(scenario_root) or [
None
]
traffic = Scenario.discover_traffic(scenario_root) or [[]]

roll_traffic = 0
roll_agent_missions = 0
roll_social_agents = 0
roll_traffic_histories = 0

if shuffle_scenarios:
roll_traffic = random.randint(0, len(traffic))
roll_agent_missions = random.randint(0, len(agent_missions))
roll_social_agents = random.randint(0, len(social_agents))
roll_traffic_histories = 0 # random.randint(0, len(traffic_histories))

Expand All @@ -253,7 +254,7 @@ def variations_for_all_scenario_roots(
concrete_traffic_history,
) in product(
np.roll(traffic, roll_traffic, 0),
np.roll(agent_missions, roll_agent_missions, 0),
mission_agent_groups,
np.roll(social_agents, roll_social_agents, 0),
np.roll(traffic_histories, roll_traffic_histories, 0),
):
Expand All @@ -276,7 +277,7 @@ def variations_for_all_scenario_roots(
scenario_root,
traffic_specs=concrete_traffic,
missions={
**(concrete_agent_missions or {}),
**{a_id: mission for a_id, mission in concrete_agent_missions},
**concrete_social_agent_missions,
},
social_agents=concrete_social_agents,
Expand Down Expand Up @@ -321,18 +322,6 @@ def discover_agent_missions(scenario_root, agents_to_be_briefed):
if not missions:
missions = [None for _ in range(len(agents_to_be_briefed))]

if len(agents_to_be_briefed) == 1:
# single-agent, so we cycle through all missions individually.
return missions
elif len(agents_to_be_briefed) > 1:
# multi-agent, so we assume missions "drive" the agents (i.e. one
# mission per agent) and we will not be cycling through missions.
assert not missions or len(missions) == len(agents_to_be_briefed), (
"You must either provide an equal number of missions ({}) to "
"agents ({}) or provide no missions at all so they can be "
"randomly generated.".format(len(missions), len(agents_to_be_briefed))
)

return missions

@staticmethod
Expand Down
2 changes: 1 addition & 1 deletion smarts/core/tests/test_observations.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ def agent_spec(agent_interface):
def env(agent_spec):
env = gym.make(
"smarts.env:hiway-v0",
scenarios=["scenarios/sumo/loop"],
scenarios=["scenarios/sumo/figure_eight"],
agent_specs={AGENT_ID: agent_spec},
headless=True,
visdom=False,
Expand Down
53 changes: 53 additions & 0 deletions smarts/core/utils/math.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
# THE SOFTWARE.
import math
from dataclasses import dataclass
from itertools import chain, permutations, product, repeat
from math import factorial
from typing import Callable, List, Sequence, Tuple, Union

Expand Down Expand Up @@ -584,3 +585,55 @@ def running_mean(prev_mean: float, prev_step: int, new_val: float) -> Tuple[floa
new_step = prev_step + 1
new_mean = prev_mean + (new_val - prev_mean) / new_step
return new_mean, new_step


def _unique_element_combination(first, second):
"""Generates a combination of set of values result groupings only contain values from unique
indices. Only works if len(first_group) <= len(second_group).
_unique_perm('ab', '123') -> a1 b2 a1 b3 a2 b1 a2 b3 a3 b1 a3 b2
_unique_perm('abc', '1') -> []

Args:
first (Sequence): A sequence of values.
second (Sequence): Another sequence of values.

Yields:
Tuple[Tuple[*Any]]: A set of values similar to permutation.
"""
result = [[a] for a in first]
sl = len(second)
rl = len(first)
perms = list(permutations(range(sl), r=rl))
for i, p in enumerate(perms):
yield tuple(
tuple(result[(i * rl + j) % rl] + [second[idx]]) for j, idx in enumerate(p)
)


def ordered_combinations(first_group, second_group, default=None):
"""Generate a product that generates that sets the values. If
len(first_group) <= len(second_group) the value is padded.

padded_product('ab', '123') -> a1 b2 a1 b3 a2 b1 a2 b3 a3 b1 a3 b2
padded_product('ab', '1', default="k") -> a1 bk ak b1
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment might be a bit misleading

  • Should it be called ordered_combinations instead of padded_product?
  • From the comment it looks like it returns a linear sequence rather than a sequence of products

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have clarified the utility and return values.

It should return groups of pairs, where the pairs use only unique indices from the contributor sequences, within each resulting group.


Args:
first_group (Sequence): A sequence of values.
second_group (Sequence): Another sequence of values.
default (Any, optional): The default values. Defaults to None.

Returns:
Generator[Tuple[Tuple[Any, ...]], None, None]: Some permutation values.
"""
len_first = len(first_group)
len_second = len(second_group)
if len_second == 0:
return product(first_group, [default])
if len_first > len_second:
return _unique_element_combination(
first_group,
list(chain(second_group, repeat(default, len_first - len_second))),
)
if len_first <= len_second:
return _unique_element_combination(first_group, second_group)
return []
28 changes: 27 additions & 1 deletion smarts/core/utils/tests/test_math.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,11 @@
# THE SOFTWARE.
import numpy as np

from smarts.core.utils.math import position_to_ego_frame, world_position_from_ego_frame
from smarts.core.utils.math import (
ordered_combinations,
position_to_ego_frame,
world_position_from_ego_frame,
)


def test_egocentric_conversion():
Expand All @@ -36,3 +40,25 @@ def test_egocentric_conversion():
p_end = world_position_from_ego_frame(pec, pe, he)

assert np.allclose(p_end, p_start)


def test_ordered_combinations():

assert not tuple(ordered_combinations("", ""))
assert tuple(ordered_combinations("a", "")) == (("a", None),)
assert tuple(ordered_combinations("abc", "12", default=1)) == (
(("a", "1"), ("b", "2"), ("c", 1)),
(("a", "1"), ("b", 1), ("c", "2")),
(("a", "2"), ("b", "1"), ("c", 1)),
(("a", "2"), ("b", 1), ("c", "1")),
(("a", 1), ("b", "1"), ("c", "2")),
(("a", 1), ("b", "2"), ("c", "1")),
)
assert tuple(ordered_combinations("ab", "123")) == (
(("a", "1"), ("b", "2")),
(("a", "1"), ("b", "3")),
(("a", "2"), ("b", "1")),
(("a", "2"), ("b", "3")),
(("a", "3"), ("b", "1")),
(("a", "3"), ("b", "2")),
)