Skip to content

Commit 9bbe625

Browse files
aszlamanuragprat1k
andauthoredJan 29, 2022
Spatial map memory (facebookresearch#937)
* shallow refactor, move ExaminedDetection from global to member of memory * forgot file * skeleton of how maps should look in place_field * fill out skeleton some * forgot some fields * fix bugs * more functional place_field, first (incomplete) integration into craftassist agent * remove obstacle from place_field map when all blocks in column removed in mc * integrate in robot * test scripts (facebookresearch#954) * tests, bugfixes, change format of self.memid2locs * bugfix * grr Co-authored-by: anuragprat1k <[email protected]>
1 parent 4c8b3e3 commit 9bbe625

File tree

10 files changed

+615
-236
lines changed

10 files changed

+615
-236
lines changed
 

‎droidlet/interpreter/robot/default_behaviors.py

+34-27
Original file line numberDiff line numberDiff line change
@@ -4,37 +4,41 @@
44
import logging
55
from droidlet.interpreter.robot import tasks
66
from droidlet.memory.robot.loco_memory_nodes import DetectedObjectNode
7-
from droidlet.lowlevel.robot_mover_utils import ExaminedMap
87
import os
98
import random
109
import numpy as np
1110
import shutil
1211
import json
1312

14-
random.seed(2021) # fixing a random seed to fix default exploration goal
13+
random.seed(2021) # fixing a random seed to fix default exploration goal
14+
1515

1616
def get_distant_goal(x, y, t, l1_thresh=35):
1717
# Get a distant goal for the slam exploration
18-
# Pick a random quadrant, get
18+
# Pick a random quadrant, get
1919
while True:
2020
xt = random.randint(-19, 19)
2121
yt = random.randint(-19, 19)
22-
d = np.linalg.norm(np.asarray([x,y]) - np.asarray([xt,yt]), ord=1)
22+
d = np.linalg.norm(np.asarray([x, y]) - np.asarray([xt, yt]), ord=1)
2323
if d > l1_thresh:
2424
return (xt, yt, 0)
2525

26+
2627
def init_logger():
27-
logger = logging.getLogger('default_behavior')
28+
logger = logging.getLogger("default_behavior")
2829
logger.setLevel(logging.INFO)
29-
fh = logging.FileHandler('default_behavior.log', 'w')
30+
fh = logging.FileHandler("default_behavior.log", "w")
3031
fh.setLevel(logging.INFO)
3132
ch = logging.StreamHandler()
3233
ch.setLevel(logging.INFO)
33-
formatter = logging.Formatter('%(asctime)s %(filename)s:%(lineno)s - %(funcName)s(): %(message)s')
34+
formatter = logging.Formatter(
35+
"%(asctime)s %(filename)s:%(lineno)s - %(funcName)s(): %(message)s"
36+
)
3437
fh.setFormatter(formatter)
3538
logger.addHandler(fh)
3639
logger.addHandler(ch)
3740

41+
3842
init_logger()
3943

4044
#TODO: Move these utils to a suitable place - as a class method in TripleNode
@@ -66,46 +70,49 @@ def start_explore(agent, goal):
6670
agent.mover.slam.reset_map()
6771
agent.mover.nav.reset_explore()
6872

69-
logger = logging.getLogger('default_behavior')
73+
logger = logging.getLogger("default_behavior")
7074
logger.info(
7175
f"Starting exploration {explore_count} \
7276
first_exploration_done {first_exploration_done} \
7377
os.getenv('CONTINUOUS_EXPLORE') {os.getenv('CONTINUOUS_EXPLORE', 'False')}"
7478
)
7579

80+
# FIXME, don't clear the memory, place_field, etc. explore more reasonably
7681
# Clear memory
7782
objects = DetectedObjectNode.get_all(agent.memory)
78-
logger.info(f'Clearing {len(objects)} memids in memory')
83+
logger.info(f"Clearing {len(objects)} memids in memory")
7984
agent.memory.clear(objects)
80-
ExaminedMap.clear()
85+
agent.memory.place_field.clear_examined()
8186
# reset object id counter
8287
agent.perception_modules["vision"].vision.deduplicate.object_id_counter = 1
8388
objects = DetectedObjectNode.get_all(agent.memory)
84-
logger.info(f'{len(objects)} memids in memory')
85-
86-
task_data = {
87-
"goal": goal,
88-
"save_data": os.getenv('SAVE_EXPLORATION', 'False') == 'True',
89+
logger.info(f"{len(objects)} memids in memory")
90+
91+
task_data = {
92+
"goal": goal,
93+
"save_data": os.getenv("SAVE_EXPLORATION", "False") == "True",
8994
"data_path": os.path.join(f"{os.getenv('HEURISTIC', 'default')}", str(explore_count)),
9095
}
91-
logger.info(f'task_data {task_data}')
92-
93-
if os.path.isdir(task_data['data_path']):
94-
shutil.rmtree(task_data['data_path'])
96+
logger.info(f"task_data {task_data}")
9597

96-
if os.getenv('HEURISTIC') in ('straightline', 'circle'):
97-
logging.info('Default behavior: Curious Exploration')
98+
if os.path.isdir(task_data["data_path"]):
99+
shutil.rmtree(task_data["data_path"])
100+
101+
if os.getenv("HEURISTIC") in ("straightline", "circle"):
102+
logging.info("Default behavior: Curious Exploration")
98103
agent.memory.task_stack_push(tasks.CuriousExplore(agent, task_data))
99104
else:
100-
logging.info('Default behavior: Default Exploration')
105+
logging.info("Default behavior: Default Exploration")
101106
agent.memory.task_stack_push(tasks.Explore(agent, task_data))
102107

103-
add_or_replace(agent, 'first_exploration_done', 'True')
104-
add_or_replace(agent, 'explore_count', str(explore_count+1))
108+
add_or_replace(agent, "first_exploration_done", "True")
109+
add_or_replace(agent, "explore_count", str(explore_count + 1))
110+
111+
105112

106113
def explore(agent):
107-
x,y,t = agent.mover.get_base_pos()
108-
goal = get_distant_goal(x,y,t)
114+
x, y, t = agent.mover.get_base_pos()
115+
goal = get_distant_goal(x, y, t)
109116
start_explore(agent, goal)
110117

111118
def get_task_data(agent):
@@ -135,4 +142,4 @@ def reexplore(agent):
135142
task_data = get_task_data(agent)
136143
logging.info(f'task_data {task_data}')
137144
if task_data is not None:
138-
agent.memory.task_stack_push(tasks.Reexplore(agent, task_data))
145+
agent.memory.task_stack_push(tasks.Reexplore(agent, task_data))

‎droidlet/interpreter/robot/tasks.py

+89-70
Large diffs are not rendered by default.

‎droidlet/lowlevel/robot_mover_utils.py

+60-103
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import shutil
1010
import glob
1111
import sys
12+
1213
if "/opt/ros/kinetic/lib/python2.7/dist-packages" in sys.path:
1314
sys.path.remove("/opt/ros/kinetic/lib/python2.7/dist-packages")
1415
import cv2
@@ -17,7 +18,7 @@
1718
from pathlib import Path
1819
import matplotlib.pyplot as plt
1920

20-
#FIXME! do this better
21+
# FIXME! do this better
2122
from .hello_robot.rotation import yaw_pitch
2223

2324
MAX_PAN_RAD = np.pi / 4
@@ -102,6 +103,7 @@ def get_move_target_for_point(base_pos, target, eps=0.5):
102103

103104
return [targetx, targetz, yaw]
104105

106+
105107
def get_step_target_for_straightline_move(base_pos, target, step_size=0.1):
106108
"""
107109
Heuristic to get step target of step_size for going to from base_pos to target
@@ -117,55 +119,64 @@ def get_step_target_for_straightline_move(base_pos, target, step_size=0.1):
117119
dx = target[0] - base_pos[0]
118120
dz = target[2] - base_pos[1]
119121

120-
if dx == 0: # vertical line
122+
if dx == 0: # vertical line
121123
theta = math.radians(90)
122124
else:
123-
theta = math.atan(abs(dz/dx))
124-
125+
theta = math.atan(abs(dz / dx))
126+
125127
signx = 1 if dx >= 0 else -1
126128
signz = 1 if dz >= 0 else -1
127-
129+
128130
targetx = base_pos[0] + signx * step_size * math.cos(theta)
129131
targetz = base_pos[1] + signz * step_size * math.sin(theta)
130132

131133
yaw, _ = get_camera_angles([targetx, CAMERA_HEIGHT, targetz], target)
132-
134+
133135
return [targetx, targetz, yaw]
134136

137+
135138
def get_straightline_path_to(target, robot_pos):
136139
pts = []
137140
cur_pos = robot_pos
138-
while np.linalg.norm(target[:2]-cur_pos[:2]) > 0.5:
141+
while np.linalg.norm(target[:2] - cur_pos[:2]) > 0.5:
139142
t = get_step_target_for_move(cur_pos, [target[0], CAMERA_HEIGHT, target[1]], step_size=0.5)
140143
pts.append(t)
141144
cur_pos = t
142145
return np.asarray(pts)
143146

147+
144148
def get_circle(r, n=10):
145-
return [[math.cos(2*math.pi/n*x)*r,math.sin(2*math.pi/n*x)*r] for x in range(0,n+1)]
149+
return [
150+
[math.cos(2 * math.pi / n * x) * r, math.sin(2 * math.pi / n * x) * r]
151+
for x in range(0, n + 1)
152+
]
153+
146154

147155
def get_circular_path(target, robot_pos, radius, num_points):
148156
"""
149157
get a circular path with num_points of radius from x
150158
xyz
151159
"""
152-
pts = get_circle(radius, num_points) # these are the x,z
160+
pts = get_circle(radius, num_points) # these are the x,z
161+
153162
def get_xyyaw(p, target):
154163
targetx = p[0] + target[0]
155164
targetz = p[1] + target[2]
156165
yaw, _ = get_camera_angles([targetx, CAMERA_HEIGHT, targetz], target)
157166
return [targetx, targetz, yaw]
158-
167+
159168
pts = np.asarray([get_xyyaw(p, target) for p in pts])
160169

161170
# find nearest pt to robot_pos as starting point
162171
def find_nearest_indx(pts, robot_pos):
163-
idx = np.asarray([np.linalg.norm(np.asarray(p[:2]) - np.asarray(robot_pos[:2])) for p in pts]).argmin()
172+
idx = np.asarray(
173+
[np.linalg.norm(np.asarray(p[:2]) - np.asarray(robot_pos[:2])) for p in pts]
174+
).argmin()
164175
return idx
165176

166177
idx = find_nearest_indx(pts, robot_pos)
167178
# rotate the pts to begin at idx
168-
pts = np.concatenate((pts[idx:,:], pts[:idx,:]), axis=0)
179+
pts = np.concatenate((pts[idx:, :], pts[:idx, :]), axis=0)
169180

170181
# TODO: get step-wise move targets to nearest point? or capture move data?
171182
# spath = get_straightline_path_to(pts[0], robot_pos)
@@ -174,9 +185,10 @@ def find_nearest_indx(pts, robot_pos):
174185

175186
return pts
176187

188+
177189
class TrajectoryDataSaver:
178190
def __init__(self, root):
179-
print(f'TrajectoryDataSaver saving to {root}')
191+
print(f"TrajectoryDataSaver saving to {root}")
180192
self.save_folder = root
181193
self.img_folder = os.path.join(self.save_folder, "rgb")
182194
self.img_folder_dbg = os.path.join(self.save_folder, "rgb_dbg")
@@ -185,27 +197,33 @@ def __init__(self, root):
185197
self.trav_folder = os.path.join(self.save_folder, "trav")
186198

187199
if os.path.exists(self.save_folder):
188-
print(f'rmtree {self.save_folder}')
200+
print(f"rmtree {self.save_folder}")
189201
shutil.rmtree(self.save_folder)
190202

191-
print(f'trying to create {self.save_folder}')
203+
print(f"trying to create {self.save_folder}")
192204
Path(self.save_folder).mkdir(parents=True, exist_ok=True)
193205

194-
for x in [self.img_folder, self.img_folder_dbg, self.depth_folder, self.seg_folder, self.trav_folder]:
206+
for x in [
207+
self.img_folder,
208+
self.img_folder_dbg,
209+
self.depth_folder,
210+
self.seg_folder,
211+
self.trav_folder,
212+
]:
195213
self.create(x)
196214

197215
self.pose_dict = {}
198216
self.pose_dict_hab = {}
199217
self.img_count = 0
200218
self.dbg_str = "None"
201219
self.init_logger()
202-
220+
203221
def init_logger(self):
204-
self.logger = logging.getLogger('trajectory_saver')
222+
self.logger = logging.getLogger("trajectory_saver")
205223
self.logger.setLevel(logging.INFO)
206-
formatter = logging.Formatter('%(filename)s:%(lineno)s - %(funcName)s(): %(message)s')
224+
formatter = logging.Formatter("%(filename)s:%(lineno)s - %(funcName)s(): %(message)s")
207225
# Enable filehandler to debug logs
208-
fh = logging.FileHandler(f"trajectory_saver.log", 'a')
226+
fh = logging.FileHandler(f"trajectory_saver.log", "a")
209227
fh.setLevel(logging.INFO)
210228
fh.setFormatter(formatter)
211229
self.logger.addHandler(fh)
@@ -219,7 +237,7 @@ def create(self, d):
219237

220238
def set_dbg_str(self, x):
221239
self.dbg_str = x
222-
240+
223241
def get_total_frames(self):
224242
return self.img_count
225243

@@ -229,20 +247,21 @@ def save(self, rgb, depth, seg, pos, habitat_pos, habitat_rot):
229247
print(f'saving {rgb.shape, depth.shape, seg.shape}')
230248
# store the images and depth
231249
rgb = cv2.cvtColor(rgb, cv2.COLOR_BGR2RGB)
232-
cv2.imwrite(
233-
self.img_folder + "/{:05d}.jpg".format(self.img_count),
250+
cv2.imwrite(self.img_folder + "/{:05d}.jpg".format(self.img_count), rgb)
251+
252+
cv2.putText(
234253
rgb,
254+
str(self.img_count) + " " + self.dbg_str,
255+
(40, 40),
256+
cv2.FONT_HERSHEY_PLAIN,
257+
1,
258+
(0, 0, 255),
235259
)
236260

237-
cv2.putText(rgb, str(self.img_count) + ' ' + self.dbg_str, (40,40), cv2.FONT_HERSHEY_PLAIN, 1, (0,0,255))
238-
239261
# robot_dbg_str = 'robot_pose ' + str(np.round(self.get_robot_global_state(), 3))
240262
# cv2.putText(rgb, robot_dbg_str, (40,60), cv2.FONT_HERSHEY_PLAIN, 1, (0,0,255))
241263

242-
cv2.imwrite(
243-
self.img_folder_dbg + "/{:05d}.jpg".format(self.img_count),
244-
rgb,
245-
)
264+
cv2.imwrite(self.img_folder_dbg + "/{:05d}.jpg".format(self.img_count), rgb)
246265

247266
# store depth
248267
np.save(self.depth_folder + "/{:05d}.npy".format(self.img_count), depth)
@@ -256,7 +275,7 @@ def save(self, rgb, depth, seg, pos, habitat_pos, habitat_rot):
256275
self.pose_dict = json.load(fp)
257276

258277
self.pose_dict[self.img_count] = copy(pos)
259-
278+
260279
with open(os.path.join(self.save_folder, "data.json"), "w") as fp:
261280
json.dump(self.pose_dict, fp)
262281

@@ -275,96 +294,34 @@ def save(self, rgb, depth, seg, pos, habitat_pos, habitat_rot):
275294

276295

277296
def visualize_examine(agent, robot_poses, object_xyz, label, obstacle_map, save_path, gt_pts=None):
278-
traj_visual_dir = os.path.join(save_path, 'traj_visual')
297+
traj_visual_dir = os.path.join(save_path, "traj_visual")
279298
if not os.path.isdir(traj_visual_dir):
280299
os.makedirs(traj_visual_dir)
281-
vis_count = len(glob.glob(traj_visual_dir + '/*.jpg'))
300+
vis_count = len(glob.glob(traj_visual_dir + "/*.jpg"))
282301
if vis_count == 0:
283302
plt.figure()
284303

285304
plt.title("Examine Visual")
286305
# visualize obstacle map
287306
if len(obstacle_map) > 0:
288307
obstacle_map = np.asarray([list(x) for x in obstacle_map])
289-
plt.plot(obstacle_map[:,1], obstacle_map[:,0], 'b+')
290-
291-
# visualize object
308+
plt.plot(obstacle_map[:, 1], obstacle_map[:, 0], "b+")
309+
310+
# visualize object
292311
if object_xyz is not None:
293-
plt.plot(object_xyz[0], object_xyz[2], 'y*')
312+
plt.plot(object_xyz[0], object_xyz[2], "y*")
294313
plt.text(object_xyz[0], object_xyz[2], label)
295-
314+
296315
# visualize robot pose
297316
if len(robot_poses) > 0:
298317
robot_poses = np.asarray(robot_poses)
299-
plt.plot(robot_poses[:,0], robot_poses[:,1], 'r--')
318+
plt.plot(robot_poses[:, 0], robot_poses[:, 1], "r--")
300319

301320
if gt_pts is not None:
302321
pts = np.asarray(gt_pts)
303-
plt.plot(pts[:,0], pts[:,1], 'y--')
304-
305-
# TODO: visualize robot heading
306-
322+
plt.plot(pts[:, 0], pts[:, 1], "y--")
323+
324+
# TODO: visualize robot heading
307325

308326
plt.savefig("{}/{:04d}.jpg".format(traj_visual_dir, vis_count))
309327
vis_count += 1
310-
311-
class ExaminedMap:
312-
"""A helper static class to maintain the state representations needed to track active exploration.
313-
droidlet.interpreter.robot.tasks.CuriousExplore uses this to decide which objects to explore next.
314-
The core of this class is the ExaminedMap.can_examine method. This is a heuristic.
315-
Long term, this information should live in memory (#FIXME @anuragprat1k).
316-
317-
It works as follows -
318-
1. for each new candidate coordinate, it fetches the closest examined coordinate.
319-
2. if this closest coordinate is within a certain threshold (1 meter) of the current coordinate,
320-
or if that region has been explored upto a certain number of times (2, for redundancy),
321-
it is not explored, since a 'close-enough' region in space has already been explored.
322-
"""
323-
examined = {}
324-
examined_id = set()
325-
last = None
326-
327-
@classmethod
328-
def l1(cls, xyz, k):
329-
""" returns the l1 distance between two standard coordinates"""
330-
return np.linalg.norm(np.asarray([xyz[0], xyz[2]]) - np.asarray([k[0], k[2]]), ord=1)
331-
332-
@classmethod
333-
def get_closest(cls, xyz):
334-
"""returns closest examined point to xyz"""
335-
c = None
336-
dist = 1.5
337-
for k, v in cls.examined.items():
338-
if cls.l1(k, xyz) < dist:
339-
dist = cls.l1(k, xyz)
340-
c = k
341-
if c is None:
342-
cls.examined[xyz] = 0
343-
return xyz
344-
return c
345-
346-
@classmethod
347-
def update(cls, target):
348-
"""called each time a region is examined. Updates relevant states."""
349-
cls.last = cls.get_closest(target['xyz'])
350-
cls.examined_id.add(target['eid'])
351-
cls.examined[cls.last] += 1
352-
353-
@classmethod
354-
def clear(cls):
355-
cls.examined = {}
356-
cls.examined_id = set()
357-
cls.last = None
358-
359-
@classmethod
360-
def can_examine(cls, x):
361-
"""decides whether to examine x or not."""
362-
loc = x['xyz']
363-
k = cls.get_closest(x['xyz'])
364-
val = True
365-
if cls.last is not None and cls.l1(cls.last, k) < 1:
366-
val = False
367-
val = cls.examined[k] < 2
368-
print(f"can_examine {x['eid'], x['label'], x['xyz'][:2]}, closest {k[:2]}, can_examine {val}")
369-
print(f"examined[k] = {cls.examined[k]}")
370-
return val

‎droidlet/memory/craftassist/mc_memory.py

+68-31
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
LocationNode,
1818
SetNode,
1919
ReferenceObjectNode,
20-
AttentionNode
20+
AttentionNode,
2121
)
2222
from .mc_memory_nodes import ( # noqa
2323
DanceNode,
@@ -100,7 +100,7 @@ def __init__(
100100
### Update world with perception updates ###
101101
############################################
102102

103-
def update(self, perception_output: namedtuple=None, areas_to_perceive: List=[]):
103+
def update(self, perception_output: namedtuple = None, areas_to_perceive: List = []):
104104
"""
105105
Updates the world with updates from agent's perception module.
106106
@@ -127,14 +127,22 @@ def update(self, perception_output: namedtuple=None, areas_to_perceive: List=[])
127127
"""Perform update the memory with input from low_level perception module"""
128128
# 1. Handle all mobs in agent's perception range
129129
if perception_output.mobs:
130+
map_changes = []
130131
for mob in perception_output.mobs:
131-
self.set_mob_position(mob)
132-
132+
mob_memid = self.set_mob_position(mob)
133+
mp = (mob.pos.x, mob.pos.y, mob.pos.z)
134+
map_changes.append(
135+
{"pos": mp, "is_obstacle": False, "memid": mob_memid, "is_move": True}
136+
)
137+
self.place_field.update_map(map_changes)
133138
# 2. Handle all items that the agent can pick up in-game
134139
if perception_output.agent_pickable_items:
140+
# FIXME PUT IN MEMORY PROPERLY
135141
# 2.1 Items that are in perception range
136142
if perception_output.agent_pickable_items["in_perception_items"]:
137-
for pickable_items in perception_output.agent_pickable_items["in_perception_items"]:
143+
for pickable_items in perception_output.agent_pickable_items[
144+
"in_perception_items"
145+
]:
138146
self.set_item_stack_position(pickable_items)
139147
# 2.2 Update previous pickable_item_stack based on perception
140148
if perception_output.agent_pickable_items["all_items"]:
@@ -154,11 +162,24 @@ def update(self, perception_output: namedtuple=None, areas_to_perceive: List=[])
154162
if perception_output.agent_attributes:
155163
agent_player = perception_output.agent_attributes
156164
memid = self.get_player_by_eid(agent_player.entityId).memid
157-
cmd = "UPDATE ReferenceObjects SET eid=?, name=?, x=?, y=?, z=?, pitch=?, yaw=? WHERE "
165+
cmd = (
166+
"UPDATE ReferenceObjects SET eid=?, name=?, x=?, y=?, z=?, pitch=?, yaw=? WHERE "
167+
)
158168
cmd = cmd + "uuid=?"
159169
self.db_write(
160-
cmd, agent_player.entityId, agent_player.name, agent_player.pos.x, agent_player.pos.y,
161-
agent_player.pos.z, agent_player.look.pitch, agent_player.look.yaw, memid
170+
cmd,
171+
agent_player.entityId,
172+
agent_player.name,
173+
agent_player.pos.x,
174+
agent_player.pos.y,
175+
agent_player.pos.z,
176+
agent_player.look.pitch,
177+
agent_player.look.yaw,
178+
memid,
179+
)
180+
ap = (agent_player.pos.x, agent_player.pos.y, agent_player.pos.z)
181+
self.place_field.update_map(
182+
[{"pos": ap, "is_obstacle": True, "memid": memid, "is_move": True}]
162183
)
163184

164185
# 4. Update other in-game players in agent's memory
@@ -170,13 +191,22 @@ def update(self, perception_output: namedtuple=None, areas_to_perceive: List=[])
170191
memid = PlayerNode.create(self, player)
171192
else:
172193
memid = mem.memid
173-
cmd = (
174-
"UPDATE ReferenceObjects SET eid=?, name=?, x=?, y=?, z=?, pitch=?, yaw=? WHERE "
175-
)
194+
cmd = "UPDATE ReferenceObjects SET eid=?, name=?, x=?, y=?, z=?, pitch=?, yaw=? WHERE "
176195
cmd = cmd + "uuid=?"
177196
self.db_write(
178-
cmd, player.entityId, player.name, player.pos.x, player.pos.y, player.pos.z,
179-
player.look.pitch, player.look.yaw, memid
197+
cmd,
198+
player.entityId,
199+
player.name,
200+
player.pos.x,
201+
player.pos.y,
202+
player.pos.z,
203+
player.look.pitch,
204+
player.look.yaw,
205+
memid,
206+
)
207+
pp = (player.pos.x, player.pos.y, player.pos.z)
208+
self.place_field.update_map(
209+
[{"pos": pp, "is_obstacle": True, "memid": memid, "is_move": True}]
180210
)
181211
memids = self._db_read_one(
182212
'SELECT uuid FROM ReferenceObjects WHERE ref_type="attention" AND type_name=?',
@@ -200,24 +230,28 @@ def update(self, perception_output: namedtuple=None, areas_to_perceive: List=[])
200230
self.maybe_remove_inst_seg(xyz)
201231

202232
# 5.2 Update agent's memory with blocks that have been destroyed.
203-
updated_areas_to_perceive = self.maybe_remove_block_from_memory(xyz, idm, areas_to_perceive)
233+
updated_areas_to_perceive = self.maybe_remove_block_from_memory(
234+
xyz, idm, areas_to_perceive
235+
)
204236

205237
# 5.3 Update blocks in memory when any change in the environment is caused either by agent or player
206-
interesting, player_placed, agent_placed = perception_output.changed_block_attributes[(xyz, idm)]
238+
interesting, player_placed, agent_placed = perception_output.changed_block_attributes[
239+
(xyz, idm)
240+
]
207241
self.maybe_add_block_to_memory(interesting, player_placed, agent_placed, xyz, idm)
208242

209243
"""Now perform update the memory with input from heuristic perception module"""
210244
# 1. Process everything in area to attend for perception
211245
if perception_output.in_perceive_area:
212246
# 1.1 Add colors of all block objects
213247
if perception_output.in_perceive_area["block_object_attributes"]:
214-
for block_object_attr in perception_output.in_perceive_area["block_object_attributes"]:
248+
for block_object_attr in perception_output.in_perceive_area[
249+
"block_object_attributes"
250+
]:
215251
block_object, color_tags = block_object_attr
216252
memid = BlockObjectNode.create(self, block_object)
217253
for color_tag in list(set(color_tags)):
218-
self.add_triple(
219-
subj=memid, pred_text="has_colour", obj_text=color_tag
220-
)
254+
self.add_triple(subj=memid, pred_text="has_colour", obj_text=color_tag)
221255
# 1.2 Update all holes with their block type in memory
222256
if perception_output.in_perceive_area["holes"]:
223257
self.add_holes_to_mem(perception_output.in_perceive_area["holes"])
@@ -233,9 +267,7 @@ def update(self, perception_output: namedtuple=None, areas_to_perceive: List=[])
233267
block_object, color_tags = block_object_attr
234268
memid = BlockObjectNode.create(self, block_object)
235269
for color_tag in list(set(color_tags)):
236-
self.add_triple(
237-
subj=memid, pred_text="has_colour", obj_text=color_tag
238-
)
270+
self.add_triple(subj=memid, pred_text="has_colour", obj_text=color_tag)
239271
# 2.2 Update all holes with their block type in memory
240272
if perception_output.near_agent["holes"]:
241273
self.add_holes_to_mem(perception_output.near_agent["holes"])
@@ -257,7 +289,6 @@ def update(self, perception_output: namedtuple=None, areas_to_perceive: List=[])
257289
output["areas_to_perceive"] = updated_areas_to_perceive
258290
return output
259291

260-
261292
def maybe_add_block_to_memory(self, interesting, player_placed, agent_placed, xyz, idm):
262293
if not interesting:
263294
return
@@ -275,13 +306,13 @@ def maybe_add_block_to_memory(self, interesting, player_placed, agent_placed, xy
275306
adjacent_memids = list(set(adjacent_memids))
276307
if len(adjacent_memids) == 0:
277308
# new block object
278-
BlockObjectNode.create(self, [(xyz, idm)])
309+
memid = BlockObjectNode.create(self, [(xyz, idm)])
310+
self.place_field.update_map([{"pos": xyz, "is_obstacle": True, "memid": memid}])
279311
elif len(adjacent_memids) == 1:
280312
# update block object
281313
memid = adjacent_memids[0]
282-
self.upsert_block(
283-
(xyz, idm), memid, "BlockObjects", player_placed, agent_placed
284-
)
314+
self.upsert_block((xyz, idm), memid, "BlockObjects", player_placed, agent_placed)
315+
self.place_field.update_map([{"pos": xyz, "is_obstacle": True, "memid": memid}])
285316
self.set_memory_updated_time(memid)
286317
self.set_memory_attended_time(memid)
287318
else:
@@ -305,7 +336,6 @@ def maybe_add_block_to_memory(self, interesting, player_placed, agent_placed, xy
305336
(xyz, idm), chosen_memid, "BlockObjects", player_placed, agent_placed
306337
)
307338

308-
309339
def add_holes_to_mem(self, holes):
310340
"""
311341
Adds the list of holes to memory and return hole memories.
@@ -326,7 +356,6 @@ def add_holes_to_mem(self, holes):
326356
hole_memories.append(self.get_mem_by_id(memid))
327357
return hole_memories
328358

329-
330359
def maybe_remove_block_from_memory(self, xyz: XYZ, idm: IDM, areas_to_perceive):
331360
"""Update agent's memory with blocks that have been destroyed."""
332361
tables = ["BlockObjects"]
@@ -340,10 +369,19 @@ def maybe_remove_block_from_memory(self, xyz: XYZ, idm: IDM, areas_to_perceive):
340369
delete = (b == 0 and idm[0] > 0) or (b > 0 and idm[0] == 0)
341370
if delete:
342371
self.remove_voxel(*xyz, table)
372+
# check if the whole column is removed:
373+
# FIXME, eventually want y slices
374+
r = self._db_read(
375+
"SELECT uuid FROM VoxelObjects WHERE x=? AND z=? and ref_type=?",
376+
xyz[0],
377+
xyz[2],
378+
tables[0],
379+
)
380+
if len(r) == 0:
381+
self.place_field.update_map([{"pos": xyz, "is_delete": True}])
343382
local_areas_to_perceive.append((xyz, 3))
344383
return local_areas_to_perceive
345384

346-
347385
def maybe_remove_inst_seg(self, xyz: XYZ):
348386
"""if the block is changed, the old instance segmentation
349387
is no longer considered valid"""
@@ -357,7 +395,6 @@ def maybe_remove_inst_seg(self, xyz: XYZ):
357395
for i in inst_seg_memids:
358396
self.forget(i[0])
359397

360-
361398
###########################
362399
### For Animate objects ###
363400
###########################

‎droidlet/memory/place_field.py

+292
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,292 @@
1+
"""
2+
Copyright (c) Facebook, Inc. and its affiliates.
3+
"""
4+
5+
import numpy as np
6+
7+
MAX_MAP_SIZE = 4097
8+
MAP_INIT_SIZE = 1025
9+
BIG_I = MAX_MAP_SIZE
10+
BIG_J = MAX_MAP_SIZE
11+
12+
13+
def no_y_l1(self, xyz, k):
14+
""" returns the l1 distance between two standard coordinates"""
15+
return np.linalg.norm(np.asarray([xyz[0], xyz[2]]) - np.asarray([k[0], k[2]]), ord=1)
16+
17+
18+
# TODO tighter integration with reference objects table, main memory update
19+
# should probably sync PlaceField maps without explicit perception updates
20+
# Node type for complicated-shaped obstacles that aren't "objects" e.g. walls?
21+
# currently just represented as occupancy cells with no memid
22+
# FIXME allow multiple memids at a single location in the map
23+
24+
25+
class PlaceField:
26+
"""
27+
maintains a grid-based map of some slice(s) of the world, and
28+
the state representations needed to track active exploration.
29+
30+
the .place_fields attribute is a dict with keys corresponding to heights,
31+
and values {"map": 2d numpy array, "updated": 2d numpy array, "memids": 2d numpy array}
32+
place_fields[h]["map"] is an occupany map at the the height h (in agent coordinates)
33+
a location is 0 if there is nothing there or it is unseen, 1 if occupied
34+
place_fields[h]["memids"] gives a memid index for the ReferenceObject at that location,
35+
if there is a ReferenceObject linked to that spatial location.
36+
the PlaceField keeps a mappping from the indices to memids in
37+
self.index2memid and self.memid2index
38+
place_fields[h]["updated"] gives the last update time of that location (in agent's internal time)
39+
if -1, it has neer been updated
40+
41+
the .map2real method converts a location from a map to world coords
42+
the .real2map method converts a location from the world to the map coords
43+
44+
droidlet.interpreter.robot.tasks.CuriousExplore uses the can_examine method to decide
45+
which objects to explore next:
46+
1. for each new candidate coordinate, it fetches the closest examined coordinate.
47+
2. if this closest coordinate is within a certain threshold (1 meter) of the current coordinate,
48+
or if that region has been explored upto a certain number of times (2, for redundancy),
49+
it is not explored, since a 'close-enough' region in space has already been explored.
50+
"""
51+
52+
def __init__(self, memory, pixels_per_unit=1):
53+
self.get_time = memory.get_time
54+
55+
self.index2memid = []
56+
self.memid2index = {}
57+
58+
self.examined = {}
59+
self.examined_id = set()
60+
self.last = None
61+
62+
self.maps = {}
63+
self.maybe_add_memid("NULL")
64+
self.maybe_add_memid(memory.self_memid)
65+
# FIXME, want slices, esp for mc... init after first perception
66+
# with h=y2slice(y) instead of using 0
67+
self.map_size = self.extend_map(h=0)
68+
69+
self.pixels_per_unit = pixels_per_unit
70+
71+
# gives an index allowing quick lookup by memid
72+
# each entry is keyed by a memid and is a dict
73+
# {str(h*BIG_I*BIG_J + i*BIG_J + j) : True}
74+
# for each placed h, i ,j
75+
self.memid2locs = {}
76+
77+
def ijh2idx(self, i, j, h):
78+
return str(h * BIG_I * BIG_J + i * BIG_J + j)
79+
80+
def idx2ijh(self, idx):
81+
idx = int(idx)
82+
j = idx % BIG_J
83+
idx = (idx - j) // BIG_J
84+
i = idx % BIG_I
85+
h = (idx - i) // BIG_I
86+
return i, j, h
87+
88+
def pop_memid_loc(self, memid, i, j, h):
89+
idx = self.hij2idx(h, i, j)
90+
del self.memid2locs[memid][idx]
91+
92+
def maybe_delete_loc(self, i, j, h, t, memid="NULL"):
93+
"""
94+
remove a loc from the maps and from memid2loc index.
95+
if memid is set, only removes the loc if the memid matches
96+
"""
97+
current_memid = self.index2memid[int(self.maps[h]["memids"][i, j])]
98+
if memid == "NULL" or current_memid == memid:
99+
self.maps[h]["memids"][i, j] = self.memid2index["NULL"]
100+
self.maps[h]["map"][i, j] = 0
101+
self.maps[h]["updated"][i, j] = t
102+
idx = self.ijh2idx(i, j, h)
103+
# maybe error/warn if its not there?
104+
if self.memid2locs.get(memid):
105+
self.memid2locs[memid].pop(idx, None)
106+
if len(self.memid2locs[memid]) == 0:
107+
self.memid2locs.pop(memid, None)
108+
109+
def delete_loc_by_memid(self, memid, t, is_move=False):
110+
"""
111+
remove all locs corresponding to a memid.
112+
if is_move is set, asserts that there is precisely one loc
113+
corresponding to the memid
114+
"""
115+
assert memid
116+
assert memid != "NULL"
117+
count = 0
118+
for idx in self.memid2locs.get(memid, []):
119+
i, j, h = self.idx2ijh(idx)
120+
self.maps[h]["memids"][i, j] = 0
121+
self.maps[h]["map"][i, j] = 0
122+
self.maps[h]["updated"][i, j] = t
123+
count = count + 1
124+
if is_move and count > 1:
125+
# eventually allow moving "large" objects
126+
raise Exception(
127+
"tried to delete more than one pixel from the place_field by memid with is_move set"
128+
)
129+
self.memid2locs.pop(memid, None)
130+
131+
def update_map(self, changes):
132+
"""
133+
changes is a list of dicts of the form
134+
{"pos": (x, y, z),
135+
"memid": str (default "NULL"),
136+
"is_obstacle": bool (default True),
137+
"is_move": bool (default False),
138+
"is_delete": bool (default False) }
139+
pos is required if is_delete is False.
140+
all other fields are always optional.
141+
142+
"is_obstacle" tells whether the agent can traverse that location
143+
if "is_move" is False, the change is taken as is; if "is_move" is True, if the
144+
change corresponds to a memid, and the memid is located somewhere on the map,
145+
the old location is removed when the new one is set. For now, to move complicated objects
146+
that cover many pixels, do not use is_move, and instead move them "by hand"
147+
by issuing a list of changes deleting the old now empty locations and adding the
148+
new now-filled locations
149+
"is_delete" True without a memid means whatever is in that location is to be removed.
150+
if a memid is set, the remove will occur only if the memid matches.
151+
152+
the "is_obstacle" status can be changed without changing memid etc.
153+
"""
154+
t = self.get_time()
155+
for c in changes:
156+
is_delete = c.get("is_delete", False)
157+
is_move = c.get("is_move", False)
158+
memid = c.get("memid", "NULL")
159+
p = c.get("pos")
160+
if p is None:
161+
assert is_delete
162+
# if the change is a remove, and is specified by memid:
163+
if not memid:
164+
raise Exception("tried to update a map location without a location or a memid")
165+
# warn if empty TODO?
166+
self.delete_loc_by_memid(memid, t)
167+
else:
168+
x, y, z = p
169+
h = self.y2slice(y)
170+
i, j = self.real2map(x, z, h)
171+
s = max(i - self.map_size + 1, j - self.map_size + 1, -i, -j)
172+
if s > 0:
173+
self.extend_map(s)
174+
i, j = self.real2map(x, z, h)
175+
s = max(i - self.map_size + 1, j - self.map_size + 1, -i, -j)
176+
if s > 0:
177+
# the map can not been extended enough to handle these bc MAX_MAP_SIZE
178+
# FIXME appropriate warning or error?
179+
continue
180+
if is_delete:
181+
self.maybe_delete_loc(i, j, h, t, memid=memid)
182+
else:
183+
if is_move:
184+
assert memid != "NULL"
185+
self.delete_loc_by_memid(memid, t, is_move=True)
186+
self.maps[h]["memids"][i, j] = self.memid2index.get(
187+
memid, self.maybe_add_memid(memid)
188+
)
189+
self.maps[h]["map"][i, j] = c.get("is_obstacle", 1)
190+
self.maps[h]["updated"][i, j] = t
191+
if not self.memid2locs.get(memid):
192+
self.memid2locs[memid] = {}
193+
self.memid2locs[memid][self.ijh2idx(i, j, h)] = True
194+
195+
# FIXME, want slices, esp for mc
196+
def y2slice(self, y):
197+
return 0
198+
199+
def real2map(self, x, z, h):
200+
"""
201+
convert an x, z coordinate in agent space to a pixel on the map
202+
"""
203+
n = self.maps[h]["map"].shape[0]
204+
i = x * self.pixels_per_unit
205+
j = z * self.pixels_per_unit
206+
i = i + n // 2
207+
j = j + n // 2
208+
return round(i), round(j)
209+
210+
def map2real(self, i, j, h):
211+
"""
212+
convert an i, j pixel coordinate in the map to agent space
213+
"""
214+
n = self.maps[h]["map"].shape[0]
215+
i = i - n // 2
216+
j = j - n // 2
217+
x = i / self.pixels_per_unit
218+
z = j / self.pixels_per_unit
219+
return x, z
220+
221+
def maybe_add_memid(self, memid):
222+
"""
223+
adds an entry to the mapping from memids to ints to put on map.
224+
these are never removed
225+
"""
226+
idx = self.memid2index.get(memid)
227+
if idx is None:
228+
idx = len(self.index2memid)
229+
self.index2memid.append(memid)
230+
self.memid2index[memid] = idx
231+
return idx
232+
233+
def extend_map(self, h=None, extension=1):
234+
assert extension >= 0
235+
if not h and len(self.maps) == 1:
236+
h = list(self.maps.keys())[0]
237+
if not self.maps.get(h):
238+
self.maps[h] = {}
239+
for m, v in {"updated": -1, "map": 0, "memids": 0}.items():
240+
self.maps[h][m] = v * np.ones((MAP_INIT_SIZE, MAP_INIT_SIZE))
241+
w = self.maps[h]["map"].shape[0]
242+
new_w = w + 2 * extension
243+
if new_w > MAX_MAP_SIZE:
244+
return -1
245+
for m, v in {"updated": -1, "map": 0, "memids": 0}.items():
246+
new_map = v * np.ones((new_w, new_w))
247+
new_map[extension:-extension, extension:-extension] = self.maps[h][m]
248+
self.maps[h][m] = new_map
249+
return new_w
250+
251+
def get_closest(self, xyz):
252+
"""returns closest examined point to xyz"""
253+
c = None
254+
dist = 1.5
255+
for k, v in self.examined.items():
256+
if no_y_l1(k, xyz) < dist:
257+
dist = no_y_l1(k, xyz)
258+
c = k
259+
if c is None:
260+
self.examined[xyz] = 0
261+
return xyz
262+
return c
263+
264+
def update(self, target):
265+
"""called each time a region is examined. Updates relevant states."""
266+
self.last = self.get_closest(target["xyz"])
267+
self.examined_id.add(target["eid"])
268+
self.examined[self.last] += 1
269+
270+
def clear_examined(self):
271+
self.examined = {}
272+
self.examined_id = set()
273+
self.last = None
274+
275+
def can_examine(self, x):
276+
"""decides whether to examine x or not."""
277+
loc = x["xyz"]
278+
k = self.get_closest(x["xyz"])
279+
val = True
280+
if self.last is not None and self.l1(cls.last, k) < 1:
281+
val = False
282+
val = self.examined[k] < 2
283+
print(
284+
f"can_examine {x['eid'], x['label'], x['xyz'][:2]}, closest {k[:2]}, can_examine {val}"
285+
)
286+
print(f"examined[k] = {self.examined[k]}")
287+
return val
288+
289+
290+
if __name__ == "__main__":
291+
W = {0: {0: {0: True}, 1: {2: {3: True}}}, 1: {5: True}}
292+
idxs = [0, 1, 2, 3]

‎droidlet/memory/robot/loco_memory.py

+20-5
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ def __init__(
4242
### Update world with perception updates ###
4343
############################################
4444

45-
def update(self, perception_output: namedtuple=None):
45+
def update(self, perception_output: namedtuple = None):
4646
"""
4747
Updates the world with updates from agent's perception module.
4848
@@ -56,13 +56,28 @@ def update(self, perception_output: namedtuple=None):
5656
return
5757
if perception_output.new_objects:
5858
for detection in perception_output.new_objects:
59-
DetectedObjectNode.create(self, detection)
59+
memid = DetectedObjectNode.create(self, detection)
60+
# TODO use the bounds, not just the center
61+
pos = (
62+
detection.get_xyz()["x"],
63+
detection.get_xyz()["y"],
64+
detection.get_xyz()["z"],
65+
)
66+
self.place_field.update_map([{"pos": pos, "memid": memid}])
6067
if perception_output.updated_objects:
6168
for detection in perception_output.updated_objects:
62-
DetectedObjectNode.update(self, detection)
69+
memid = DetectedObjectNode.update(self, detection)
70+
# TODO use the bounds, not just the center
71+
pos = (
72+
detection.get_xyz()["x"],
73+
detection.get_xyz()["y"],
74+
detection.get_xyz()["z"],
75+
)
76+
self.place_field.update_map([{"pos": pos, "memid": memid, "is_move": True}])
6377
if perception_output.humans:
6478
for human in perception_output.humans:
6579
HumanPoseNode.create(self, human)
80+
# FIXME, not putting in map, need to dedup?
6681

6782
#################
6883
### Players ###
@@ -97,5 +112,5 @@ def add_dance(self, dance_fn, name=None, tags=[]):
97112

98113
def clear(self, objects):
99114
for o in objects:
100-
if o['memid'] != self.self_memid:
101-
self.forget(o['memid'])
115+
if o["memid"] != self.self_memid:
116+
self.forget(o["memid"])

‎droidlet/memory/sql_memory.py

+2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from droidlet.memory.memory_filters import MemorySearcher
1919
from droidlet.event import dispatch
2020
from droidlet.memory.memory_util import parse_sql, format_query
21+
from droidlet.memory.place_field import PlaceField
2122

2223
from droidlet.memory.memory_nodes import ( # noqa
2324
TaskNode,
@@ -136,6 +137,7 @@ def __init__(
136137
self.tag(self.self_memid, "SELF")
137138

138139
self.searcher = MemorySearcher()
140+
self.place_field = PlaceField(self)
139141

140142
def __del__(self):
141143
"""Close the database file"""

‎droidlet/memory/tests/test_low_level_memory.py

+35
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
Copyright (c) Facebook, Inc. and its affiliates.
33
"""
44
import unittest
5+
import numpy as np
56
from droidlet.memory.memory_nodes import (
67
SelfNode,
78
PlayerNode,
@@ -316,5 +317,39 @@ def test_triggers(self):
316317
assert len(triples) == 0
317318

318319

320+
class PlaceFieldTest(unittest.TestCase):
321+
def test_place_field(self):
322+
memory = AgentMemory()
323+
PF = memory.place_field
324+
joe_x = 1
325+
joe_z = 2
326+
joe_loc = (joe_x, 0, joe_z)
327+
jane_loc = (-1, 0, 1)
328+
joe_memid = PlayerNode.create(memory, Player(10, "joe", Pos(*joe_loc), Look(0, 0)))
329+
jane_memid = PlayerNode.create(memory, Player(11, "jane", Pos(*jane_loc), Look(0, 0)))
330+
wall_locs = [{"pos": (-i, 0, 4)} for i in range(5)]
331+
changes = [{"pos": joe_loc, "memid": joe_memid}, {"pos": jane_loc, "memid": jane_memid}]
332+
changes.extend(wall_locs)
333+
PF.update_map(changes)
334+
assert PF.maps[0]["map"].sum() == 7
335+
jl = PF.memid2locs[joe_memid]
336+
assert len(jl) == 1
337+
recovered_pos = tuple(int(i) for i in PF.map2real(*PF.idx2ijh(list(jl.keys())[0])))
338+
assert recovered_pos == (joe_x, joe_z)
339+
assert len(PF.memid2locs["NULL"]) == 5
340+
changes = [{"pos": (-1, 0, 4), "is_delete": True}]
341+
PF.update_map(changes)
342+
assert len(PF.memid2locs["NULL"]) == 4
343+
new_jane_x = -5
344+
new_jane_z = 5
345+
changes = [{"pos": (new_jane_x, 0, new_jane_z), "memid": jane_memid, "is_move": True}]
346+
PF.update_map(changes)
347+
jl = PF.memid2locs[jane_memid]
348+
assert len(jl) == 1
349+
recovered_pos = tuple(int(i) for i in PF.map2real(*PF.idx2ijh(list(jl.keys())[0])))
350+
assert recovered_pos == (new_jane_x, new_jane_z)
351+
assert PF.maps[0]["map"].sum() == 6
352+
353+
319354
if __name__ == "__main__":
320355
unittest.main()

‎launch_agent_collect.sh

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
default_ip=$(hostname -I | cut -f1 -d" ")
2+
ip=${LOCOBOT_IP:-$default_ip}
3+
echo "default_ip" $default_ip
4+
export LOCOBOT_IP=$ip
5+
export SAVE_EXPLORATION=True
6+
export DATA_PATH=straightline_test
7+
export HEURISTIC=straightline
8+
export VISUALIZE_EXAMINE=True
9+
export CONTINUOUS_EXPLORE=False
10+
source activate /private/home/apratik/miniconda3/envs/droidlet
11+
python agents/locobot/locobot_agent.py --dev

‎launch_env.sh

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export NOISY_HABITAT=False
2+
export ADD_HUMANS=False
3+
source activate /private/home/apratik/miniconda3/envs/droidlet
4+
./droidlet/lowlevel/locobot/remote/launch_pyro_habitat.sh --scene_path /checkpoint/apratik/replica/apartment_0/habitat/mesh_semantic.ply

0 commit comments

Comments
 (0)
Please sign in to comment.