Skip to content

Commit 6f20110

Browse files
authored
Merge pull request #41 from bmielnicki/recipes_generation
Recipes generation
2 parents f1669a1 + af85350 commit 6f20110

File tree

3 files changed

+193
-27
lines changed

3 files changed

+193
-27
lines changed

src/overcooked_ai_py/mdp/layout_generator.py

Lines changed: 70 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import numpy as np
2-
import random
2+
3+
import random, copy
34
from overcooked_ai_py.utils import rnd_int_uniform, rnd_uniform
45
from overcooked_ai_py.mdp.actions import Action, Direction
5-
from overcooked_ai_py.mdp.overcooked_mdp import OvercookedGridworld
6+
from overcooked_ai_py.mdp.overcooked_mdp import OvercookedGridworld, Recipe
67

78
EMPTY = ' '
89
COUNTER = 'X'
@@ -81,6 +82,7 @@ def generate(self, outside_information={}):
8182
mdp_params = self.params_schedule_fn(outside_information)
8283
return mdp_params
8384

85+
DEFAULT_FEATURE_TYPES = (POT, ONION_DISPENSER, DISH_DISPENSER, SERVING_LOC) # NOTE: TOMATO_DISPENSER is disabled by default
8486

8587
class LayoutGenerator(object):
8688
# NOTE: This class hasn't been tested extensively.
@@ -126,36 +128,71 @@ def generate_padded_mdp(self, outside_information={}):
126128
Return a PADDED MDP with mdp params specified in self.mdp_params
127129
"""
128130
mdp_gen_params = self.mdp_params_generator.generate(outside_information)
131+
129132
outer_shape = self.outer_shape
130133
if "layout_name" in mdp_gen_params.keys() and mdp_gen_params["layout_name"] is not None:
131134
mdp = OvercookedGridworld.from_layout_name(**mdp_gen_params)
132135
mdp_generator_fn = lambda: self.padded_mdp(mdp)
133136
else:
134-
135-
required_keys = ["inner_shape", "prop_empty", "prop_feats", "display", "start_all_orders"]
137+
required_keys = ["inner_shape", "prop_empty", "prop_feats", "display"]
138+
# with generate_all_orders key start_all_orders will be generated inside make_new_layout method
139+
if not mdp_gen_params.get("generate_all_orders"):
140+
required_keys.append("start_all_orders")
136141
missing_keys = [k for k in required_keys if k not in mdp_gen_params.keys()]
142+
if len(missing_keys) != 0:
143+
print("missing keys dict", mdp_gen_params)
137144
assert len(missing_keys) == 0, "These keys were missing from the mdp_params: {}".format(missing_keys)
138-
139-
recipe_params = {"start_all_orders": mdp_gen_params["start_all_orders"]}
140-
if "recipe_values" in mdp_gen_params:
141-
recipe_params["recipe_values"] = mdp_gen_params["recipe_values"]
142-
if "recipe_times" in mdp_gen_params:
143-
recipe_params["recipe_times"] = mdp_gen_params["recipe_times"]
144-
145145
inner_shape = mdp_gen_params["inner_shape"]
146146
assert inner_shape[0] <= outer_shape[0] and inner_shape[1] <= outer_shape[1], \
147147
"inner_shape cannot fit into the outershap"
148-
149148
layout_generator = LayoutGenerator(self.mdp_params_generator, outer_shape=self.outer_shape)
150-
mdp_generator_fn = lambda: layout_generator.make_disjoint_sets_layout(
151-
inner_shape=mdp_gen_params["inner_shape"],
152-
prop_empty=mdp_gen_params["prop_empty"],
153-
prop_features=mdp_gen_params["prop_feats"],
154-
base_param=recipe_params,
155-
display=mdp_gen_params["display"]
156-
)
157-
149+
150+
if "feature_types" not in mdp_gen_params:
151+
mdp_gen_params["feature_types"] = DEFAULT_FEATURE_TYPES
152+
153+
mdp_generator_fn = lambda: layout_generator.make_new_layout(mdp_gen_params)
158154
return mdp_generator_fn()
155+
156+
@staticmethod
157+
def create_base_params(mdp_gen_params):
158+
assert mdp_gen_params.get("start_all_orders") or mdp_gen_params.get("generate_all_orders")
159+
mdp_gen_params = LayoutGenerator.add_generated_mdp_params_orders(mdp_gen_params)
160+
recipe_params = {"start_all_orders": mdp_gen_params["start_all_orders"]}
161+
if mdp_gen_params.get("start_bonus_orders"):
162+
recipe_params["start_bonus_orders"] = mdp_gen_params["start_bonus_orders"]
163+
if "recipe_values" in mdp_gen_params:
164+
recipe_params["recipe_values"] = mdp_gen_params["recipe_values"]
165+
if "recipe_times" in mdp_gen_params:
166+
recipe_params["recipe_times"] = mdp_gen_params["recipe_times"]
167+
return recipe_params
168+
169+
@staticmethod
170+
def add_generated_mdp_params_orders(mdp_params):
171+
"""
172+
adds generated parameters (i.e. generated orders) to mdp_params,
173+
returns onchanged copy of mdp_params when there is no "generate_all_orders" and "generate_bonus_orders" keys inside mdp_params
174+
"""
175+
mdp_params = copy.deepcopy(mdp_params)
176+
if mdp_params.get("generate_all_orders"):
177+
all_orders_kwargs = copy.deepcopy(mdp_params["generate_all_orders"])
178+
179+
if all_orders_kwargs.get("recipes"):
180+
all_orders_kwargs["recipes"] = [Recipe.from_dict(r) for r in all_orders_kwargs["recipes"]]
181+
182+
all_recipes = Recipe.generate_random_recipes(**all_orders_kwargs)
183+
mdp_params["start_all_orders"] = [r.to_dict() for r in all_recipes]
184+
else:
185+
all_recipes = Recipe.ALL_RECIPES
186+
187+
if mdp_params.get("generate_bonus_orders"):
188+
bonus_orders_kwargs = copy.deepcopy(mdp_params["generate_bonus_orders"])
189+
190+
if not bonus_orders_kwargs.get("recipes"):
191+
bonus_orders_kwargs["recipes"] = all_recipes
192+
193+
bonus_recipes = Recipe.generate_random_recipes(**bonus_orders_kwargs)
194+
mdp_params["start_bonus_orders"] = [r.to_dict() for r in bonus_recipes]
195+
return mdp_params
159196

160197
def padded_mdp(self, mdp, display=False):
161198
"""Returns a padded MDP from an MDP"""
@@ -166,10 +203,20 @@ def padded_mdp(self, mdp, display=False):
166203
mdp_grid = self.padded_grid_to_layout_grid(padded_grid, start_positions, display=display)
167204
return OvercookedGridworld.from_grid(mdp_grid)
168205

169-
def make_disjoint_sets_layout(self, inner_shape, prop_empty, prop_features, base_param={}, display=True):
206+
def make_new_layout(self, mdp_gen_params):
207+
return self.make_disjoint_sets_layout(
208+
inner_shape=mdp_gen_params["inner_shape"],
209+
prop_empty=mdp_gen_params["prop_empty"],
210+
prop_features=mdp_gen_params["prop_feats"],
211+
base_param=LayoutGenerator.create_base_params(mdp_gen_params),
212+
feature_types=mdp_gen_params["feature_types"],
213+
display=mdp_gen_params["display"]
214+
)
215+
216+
def make_disjoint_sets_layout(self, inner_shape, prop_empty, prop_features, base_param, feature_types=DEFAULT_FEATURE_TYPES, display=True):
170217
grid = Grid(inner_shape)
171218
self.dig_space_with_disjoint_sets(grid, prop_empty)
172-
self.add_features(grid, prop_features)
219+
self.add_features(grid, prop_features, feature_types)
173220

174221
padded_grid = self.embed_grid(grid)
175222
start_positions = self.get_random_starting_positions(padded_grid)
@@ -242,12 +289,10 @@ def dig_space_with_fringe_expansion(self, grid, prop_empty=0.1):
242289
if grid.is_valid_dig_location(location):
243290
fringe.add(location)
244291

245-
def add_features(self, grid, prop_features=0):
292+
def add_features(self, grid, prop_features=0, feature_types=DEFAULT_FEATURE_TYPES):
246293
"""
247294
Places one round of basic features and then adds random features
248295
until prop_features of valid locations are filled"""
249-
feature_types = [POT, ONION_DISPENSER, DISH_DISPENSER, SERVING_LOC]
250-
# feature_types = [POT, ONION_DISPENSER, TOMATO_DISPENSER, DISH_DISPENSER, SERVING_LOC] # NOTE: currently disabled TOMATO_DISPENSER
251296

252297
valid_locations = grid.valid_feature_locations()
253298
np.random.shuffle(valid_locations)

src/overcooked_ai_py/mdp/overcooked_mdp.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,34 @@ def configure(cls, conf):
236236

237237
if 'onion_value' in conf:
238238
cls._onion_value = conf['onion_value']
239+
240+
@classmethod
241+
def generate_random_recipes(cls, n=1, min_size=2, max_size=3, ingredients=None, recipes=None, unique=True):
242+
"""
243+
n (int): how many recipes generate
244+
min_size (int): min generated recipe size
245+
max_size (int): max generated recipe size
246+
ingredients (list(str)): list of ingredients used for generating recipes (default is cls.ALL_INGREDIENTS)
247+
recipes (list(Recipe)): list of recipes to choose from (default is cls.ALL_RECIPES)
248+
unique (bool): if all recipes are unique (without repeats)
249+
"""
250+
if recipes is None: recipes = cls.ALL_RECIPES
251+
252+
ingredients = set(ingredients or cls.ALL_INGREDIENTS)
253+
choice_replace = not(unique)
254+
255+
assert 1 <= min_size <= max_size <= cls.MAX_NUM_INGREDIENTS
256+
assert all(ingredient in cls.ALL_INGREDIENTS for ingredient in ingredients)
257+
258+
def valid_size(r):
259+
return min_size <= len(r.ingredients) <= max_size
260+
261+
def valid_ingredients(r):
262+
return all(i in ingredients for i in r.ingredients)
263+
264+
relevant_recipes = [r for r in recipes if valid_size(r) and valid_ingredients(r)]
265+
assert choice_replace or (n <= len(relevant_recipes))
266+
return np.random.choice(relevant_recipes, n, replace=choice_replace)
239267

240268
@classmethod
241269
def from_dict(cls, obj_dict):

testing/overcooked_test.py

Lines changed: 95 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
import unittest, os
2+
import json
23
import numpy as np
34
from math import factorial
45
from overcooked_ai_py.mdp.actions import Action, Direction
56
from overcooked_ai_py.mdp.overcooked_mdp import PlayerState, OvercookedGridworld, OvercookedState, ObjectState, SoupState, Recipe
67
from overcooked_ai_py.mdp.overcooked_env import OvercookedEnv, DEFAULT_ENV_PARAMS
7-
from overcooked_ai_py.mdp.layout_generator import LayoutGenerator
8+
from overcooked_ai_py.mdp.layout_generator import LayoutGenerator, ONION_DISPENSER, TOMATO_DISPENSER, POT, DISH_DISPENSER, SERVING_LOC
89
from overcooked_ai_py.agents.agent import AgentGroup, AgentPair, GreedyHumanModel, FixedPlanAgent, RandomAgent
910
from overcooked_ai_py.agents.benchmarking import AgentEvaluator
1011
from overcooked_ai_py.planning.planners import MediumLevelActionManager, NO_COUNTERS_PARAMS, MotionPlanner
1112
from overcooked_ai_py.utils import save_pickle, load_pickle, iterate_over_json_files_in_dir, load_from_json, save_as_json
1213
from utils import TESTING_DATA_DIR, generate_serialized_trajectory
1314

14-
1515
START_ORDER_LIST = ["any"]
1616
n, s = Direction.NORTH, Direction.SOUTH
1717
e, w = Direction.EAST, Direction.WEST
@@ -80,6 +80,24 @@ def test_invalid_input(self):
8080
self.assertRaises(ValueError, Recipe, [])
8181
self.assertRaises(ValueError, Recipe, "invalid argument")
8282

83+
def test_recipes_generation(self):
84+
self.assertRaises(AssertionError, Recipe.generate_random_recipes, max_size=Recipe.MAX_NUM_INGREDIENTS+1)
85+
self.assertRaises(AssertionError, Recipe.generate_random_recipes, min_size=0)
86+
self.assertRaises(AssertionError, Recipe.generate_random_recipes, min_size=3, max_size=2)
87+
self.assertRaises(AssertionError, Recipe.generate_random_recipes, ingredients=["onion", "tomato", "fake_ingredient"])
88+
self.assertRaises(AssertionError, Recipe.generate_random_recipes, n=99999)
89+
self.assertEqual(len(Recipe.generate_random_recipes(n=3)), 3)
90+
self.assertEqual(len(Recipe.generate_random_recipes(n=99, unique=False)), 99)
91+
92+
two_sized_recipes = [Recipe(["onion", "onion"]), Recipe(["onion", "tomato"]), Recipe(["tomato", "tomato"])]
93+
for _ in range(100):
94+
self.assertCountEqual(two_sized_recipes, Recipe.generate_random_recipes(n=3, min_size=2, max_size=2, ingredients=["onion", "tomato"]))
95+
96+
only_onions_recipes = [Recipe(["onion", "onion"]), Recipe(["onion", "onion", "onion"])]
97+
for _ in range(100):
98+
self.assertCountEqual(only_onions_recipes, Recipe.generate_random_recipes(n=2, min_size=2, max_size=3, ingredients=["onion"]))
99+
100+
self.assertCountEqual(only_onions_recipes, set([Recipe.generate_random_recipes(n=1, recipes=only_onions_recipes)[0] for _ in range(100)])) # false positives rate for this test is 1/10^99
83101

84102
def _expected_num_recipes(self, num_ingredients, max_len):
85103
return comb(num_ingredients + max_len, num_ingredients) - 1
@@ -873,6 +891,81 @@ def test_random_layout(self):
873891
all_same_layout = all([np.array_equal(env.mdp.terrain_mtx, terrain) for terrain in layouts_seen])
874892
self.assertFalse(all_same_layout)
875893

894+
def test_random_layout_feature_types(self):
895+
mandatory_features = {POT, DISH_DISPENSER, SERVING_LOC}
896+
optional_features = {ONION_DISPENSER, TOMATO_DISPENSER}
897+
optional_features_combinations = [{ONION_DISPENSER, TOMATO_DISPENSER}, {ONION_DISPENSER}, {TOMATO_DISPENSER}]
898+
899+
for optional_features_combo in optional_features_combinations:
900+
left_out_optional_features = optional_features - optional_features_combo
901+
used_features = list(optional_features_combo | mandatory_features)
902+
mdp_gen_params = {"prop_feats": 0.9,
903+
"feature_types": used_features,
904+
"prop_empty": 0.1,
905+
"inner_shape": (6, 5),
906+
"display": False,
907+
"start_all_orders" : [
908+
{ "ingredients" : ["onion", "onion", "onion"]}
909+
]}
910+
mdp_fn = LayoutGenerator.mdp_gen_fn_from_dict(mdp_gen_params, outer_shape=(6, 5))
911+
env = OvercookedEnv(mdp_fn, **DEFAULT_ENV_PARAMS)
912+
for _ in range(10):
913+
env.reset()
914+
curr_terrain = env.mdp.terrain_mtx
915+
terrain_features = set.union(*(set(line) for line in curr_terrain))
916+
self.assertTrue(all(elem in terrain_features for elem in used_features)) # all used_features are actually used
917+
if left_out_optional_features:
918+
self.assertFalse(any(elem in terrain_features for elem in left_out_optional_features)) # all left_out optional_features are not used
919+
920+
def test_random_layout_generated_recipes(self):
921+
only_onions_recipes = [Recipe(["onion", "onion"]), Recipe(["onion", "onion", "onion"])]
922+
only_onions_dict_recipes = [r.to_dict() for r in only_onions_recipes]
923+
924+
# checking if recipes are generated from mdp_params
925+
mdp_gen_params = {"generate_all_orders": {"n":2, "ingredients": ["onion"], "min_size":2, "max_size":3},
926+
"prop_feats": 0.9,
927+
"prop_empty": 0.1,
928+
"inner_shape": (6, 5),
929+
"display": False}
930+
mdp_fn = LayoutGenerator.mdp_gen_fn_from_dict(mdp_gen_params, outer_shape=(6, 5))
931+
env = OvercookedEnv(mdp_fn, **DEFAULT_ENV_PARAMS)
932+
for _ in range(10):
933+
env.reset()
934+
self.assertCountEqual(env.mdp.start_all_orders, only_onions_dict_recipes)
935+
self.assertEqual(len(env.mdp.start_bonus_orders), 0)
936+
937+
# checking if bonus_orders is subset of all_orders even if not specified
938+
939+
mdp_gen_params = {"generate_all_orders": {"n":2, "ingredients": ["onion"], "min_size":2, "max_size":3},
940+
"generate_bonus_orders": {"n":1, "min_size":2, "max_size":3},
941+
"prop_feats": 0.9,
942+
"prop_empty": 0.1,
943+
"inner_shape": (6, 5),
944+
"display": False}
945+
mdp_fn = LayoutGenerator.mdp_gen_fn_from_dict(mdp_gen_params, outer_shape=(6,5))
946+
env = OvercookedEnv(mdp_fn, **DEFAULT_ENV_PARAMS)
947+
for _ in range(10):
948+
env.reset()
949+
self.assertCountEqual(env.mdp.start_all_orders, only_onions_dict_recipes)
950+
self.assertEqual(len(env.mdp.start_bonus_orders), 1)
951+
self.assertTrue(env.mdp.start_bonus_orders[0] in only_onions_dict_recipes)
952+
953+
# checking if after reset there are new recipes generated
954+
mdp_gen_params = {"generate_all_orders": {"n":3, "min_size":2, "max_size":3},
955+
"prop_feats": 0.9,
956+
"prop_empty": 0.1,
957+
"inner_shape": (6, 5),
958+
"display": False,
959+
"feature_types": [POT, DISH_DISPENSER, SERVING_LOC, ONION_DISPENSER, TOMATO_DISPENSER]
960+
}
961+
mdp_fn = LayoutGenerator.mdp_gen_fn_from_dict(mdp_gen_params, outer_shape=(6,5))
962+
env = OvercookedEnv(mdp_fn, **DEFAULT_ENV_PARAMS)
963+
generated_recipes_strings = set()
964+
for _ in range(20):
965+
env.reset()
966+
generated_recipes_strings |= {json.dumps(o, sort_keys=True) for o in env.mdp.start_all_orders}
967+
self.assertTrue(len(generated_recipes_strings) > 3)
968+
876969

877970
class TestGymEnvironment(unittest.TestCase):
878971

0 commit comments

Comments
 (0)