Skip to content

Commit aef8d40

Browse files
[BugFix, Feature] GNN position_key and velocity_key not in observation_spec (facebookresearch#125)
* amend * amend * amend * amend * amend * amend * amend * amend * amend
1 parent d260eea commit aef8d40

4 files changed

Lines changed: 97 additions & 55 deletions

File tree

benchmarl/conf/model/layers/gnn.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ gnn_kwargs:
88
aggr: "add"
99

1010
position_key: null
11+
pos_features: 0
1112
velocity_key: null
13+
vel_features: 0
1214

1315
exclude_pos_from_node_features: False
1416
edge_radius: null

benchmarl/experiment/experiment.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -715,7 +715,6 @@ def _get_excluded_keys(self, group: str):
715715
for other_group in self.group_map.keys():
716716
if other_group != group:
717717
excluded_keys += [other_group, ("next", other_group)]
718-
excluded_keys += ["info", (group, "info"), ("next", group, "info")]
719718
return excluded_keys
720719

721720
def _optimizer_loop(self, group: str) -> TensorDictBase:

benchmarl/models/gnn.py

Lines changed: 92 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@
1111
import warnings
1212
from dataclasses import dataclass, MISSING
1313
from math import prod
14-
from typing import Optional, Type
14+
from typing import List, Optional, Type
1515

1616
import torch
1717
from tensordict import TensorDictBase
18-
from tensordict.utils import _unravel_key_to_tuple
18+
from tensordict.utils import _unravel_key_to_tuple, NestedKey
1919
from torch import nn, Tensor
2020

2121
from benchmarl.models.common import Model, ModelConfig
@@ -59,16 +59,21 @@ class Gnn(Model):
5959
self_loops (str): Whether the resulting adjacency matrix will have self loops.
6060
gnn_class (Type[torch_geometric.nn.MessagePassing]): the gnn convolution class to use
6161
gnn_kwargs (dict, optional): the dict of arguments to pass to the gnn conv class
62-
position_key (str, optional): if provided, it will need to match a leaf key in the env observation spec
63-
representing the agent position. This key will not be processed as a node feature, but it will used to construct
64-
edge features. In particular it be used to compute relative positions (``pos_node_1 - pos_node_2``) and a
62+
position_key (str, optional): if provided, it will need to match a leaf key in the tensordict coming from the env
63+
(we suggest to use the "info" dict) representing the agent position. This key will be processed as a
64+
node feature (unless exclude_pos_from_node_features=True) and it will be used to construct edge features.
65+
In particular, it will be used to compute relative positions (``pos_node_1 - pos_node_2``) and a
6566
one-dimensional distance for all neighbours in the graph.
67+
pos_features (int, optional): Needed when position_key is specified.
68+
It has to match to the last element of the shape the tensor under position_key.
6669
exclude_pos_from_node_features (optional, bool): If ``position_key`` is provided,
6770
wether to use it just to compute edge features or also include it in node features.
68-
velocity_key (str, optional): if provided, it will need to match a leaf key in the env observation spec
69-
representing the agent velocity. This key will not be processed as a node feature, but it will used to construct
70-
edge features. In particular it be used to compute relative velocities (``vel_node_1 - vel_node_2``) for all neighbours
71-
in the graph.
71+
velocity_key (str, optional): if provided, it will need to match a leaf key in the tensordict coming from the env
72+
(we suggest to use the "info" dict) representing the agent velocity. This key will be processed as a node feature, and
73+
it will be used to construct edge features. In particular, it will be used to compute relative velocities
74+
(``vel_node_1 - vel_node_2``) for all neighbours in the graph.
75+
vel_features (int, optional): Needed when velocity_key is specified.
76+
It has to match to the last element of the shape the tensor under velocity_key.
7277
edge_radius (float, optional): If topology is ``"from_pos"`` the radius to use to build the agent graph.
7378
Agents within this radius distance will be neighnours.
7479
@@ -120,6 +125,8 @@ def __init__(
120125
exclude_pos_from_node_features: Optional[bool],
121126
velocity_key: Optional[str],
122127
edge_radius: Optional[float],
128+
pos_features: Optional[int],
129+
vel_features: Optional[int],
123130
**kwargs,
124131
):
125132
self.topology = topology
@@ -128,34 +135,26 @@ def __init__(
128135
self.velocity_key = velocity_key
129136
self.exclude_pos_from_node_features = exclude_pos_from_node_features
130137
self.edge_radius = edge_radius
138+
self.pos_features = pos_features
139+
self.vel_features = vel_features
131140

132141
super().__init__(**kwargs)
133142

134-
self.pos_features = sum(
135-
[
136-
spec.shape[-1]
137-
for key, spec in self.input_spec.items(True, True)
138-
if _unravel_key_to_tuple(key)[-1] == position_key
139-
]
140-
) # Input keys ending with `position_key`
141143
if self.pos_features > 0:
142144
self.pos_features += 1 # We will add also 1-dimensional distance
143-
self.vel_features = sum(
144-
[
145-
spec.shape[-1]
146-
for key, spec in self.input_spec.items(True, True)
147-
if _unravel_key_to_tuple(key)[-1] == velocity_key
148-
]
149-
) # Input keys ending with `velocity_key`
150145
self.edge_features = self.pos_features + self.vel_features
151146
self.input_features = sum(
152147
[
153148
spec.shape[-1]
154149
for key, spec in self.input_spec.items(True, True)
155-
if _unravel_key_to_tuple(key)[-1]
156-
not in ((position_key) if self.exclude_pos_from_node_features else ())
150+
if _unravel_key_to_tuple(key)[-1] not in (position_key, velocity_key)
157151
]
158-
) # Input keys not ending with `velocity_key` and `position_key`
152+
) # Input keys
153+
if self.position_key is not None and not self.exclude_pos_from_node_features:
154+
self.input_features += self.pos_features - 1
155+
if self.velocity_key is not None:
156+
self.input_features += self.vel_features
157+
159158
self.output_features = self.output_leaf_spec.shape[-1]
160159

161160
if gnn_kwargs is None:
@@ -191,6 +190,8 @@ def __init__(
191190
device=self.device,
192191
n_agents=self.n_agents,
193192
)
193+
self._full_position_key = None
194+
self._full_velocity_key = None
194195

195196
def _perform_checks(self):
196197
super()._perform_checks()
@@ -208,6 +209,22 @@ def _perform_checks(self):
208209
raise ValueError(
209210
"exclude_pos_from_node_features needs to be specified when position_key is provided"
210211
)
212+
if self.position_key is not None and self.pos_features <= 0:
213+
raise ValueError(
214+
f"Position key specified but pos_features is {self.pos_features}"
215+
)
216+
elif self.position_key is None and self.pos_features > 0:
217+
raise ValueError(
218+
f"If no position_key is given, pos_features needs to be 0, got: {self.pos_features}"
219+
)
220+
if self.velocity_key is not None and self.vel_features <= 0:
221+
raise ValueError(
222+
f"Velocity key specified but vel_features is {self.vel_features}"
223+
)
224+
elif self.velocity_key is None and self.vel_features > 0:
225+
raise ValueError(
226+
f"If no velocity_key is given, vel_features needs to be 0, got: {self.vel_features}"
227+
)
211228

212229
if not self.input_has_agent_dim:
213230
raise ValueError(
@@ -247,40 +264,51 @@ def _perform_checks(self):
247264

248265
def _forward(self, tensordict: TensorDictBase) -> TensorDictBase:
249266
# Gather in_key
250-
input = torch.cat(
251-
[
252-
tensordict.get(in_key)
253-
for in_key in self.in_keys
254-
if _unravel_key_to_tuple(in_key)[-1]
255-
not in (
256-
(self.position_key) if self.exclude_pos_from_node_features else ()
257-
)
258-
],
259-
dim=-1,
260-
)
267+
input = [
268+
tensordict.get(in_key)
269+
for in_key in self.in_keys
270+
if _unravel_key_to_tuple(in_key)[-1]
271+
not in (self.position_key, self.velocity_key)
272+
]
273+
274+
# Retrieve position
261275
if self.position_key is not None:
262-
pos = torch.cat(
263-
[
264-
tensordict.get(in_key)
265-
for in_key in self.in_keys
266-
if _unravel_key_to_tuple(in_key)[-1] == self.position_key
267-
],
268-
dim=-1,
269-
)
276+
if self._full_position_key is None: # Run once to find full key
277+
self._full_position_key = self._get_key_terminating_with(
278+
list(tensordict.keys(True, True)), self.position_key
279+
)
280+
pos = tensordict.get(self._full_position_key)
281+
if pos.shape[-1] != self.pos_features - 1:
282+
raise ValueError(
283+
f"Position key in tensordict is {pos.shape[-1]}-dimensional, "
284+
f"while model was configured with pos_features={self.pos_features-1}"
285+
)
286+
else:
287+
pos = tensordict.get(self._full_position_key)
288+
if not self.exclude_pos_from_node_features:
289+
input.append(pos)
270290
else:
271291
pos = None
292+
293+
# Retrieve velocity
272294
if self.velocity_key is not None:
273-
vel = torch.cat(
274-
[
275-
tensordict.get(in_key)
276-
for in_key in self.in_keys
277-
if _unravel_key_to_tuple(in_key)[-1] == self.velocity_key
278-
],
279-
dim=-1,
280-
)
295+
if self._full_velocity_key is None: # Run once to find full key
296+
self._full_velocity_key = self._get_key_terminating_with(
297+
list(tensordict.keys(True, True)), self.velocity_key
298+
)
299+
vel = tensordict.get(self._full_velocity_key)
300+
if vel.shape[-1] != self.vel_features:
301+
raise ValueError(
302+
f"Velocity key in tensordict is {vel.shape[-1]}-dimensional, "
303+
f"while model was configured with vel_features={self.vel_features}"
304+
)
305+
else:
306+
vel = tensordict.get(self._full_velocity_key)
307+
input.append(vel)
281308
else:
282309
vel = None
283310

311+
input = torch.cat(input, dim=-1)
284312
batch_size = input.shape[:-2]
285313

286314
graph = _batch_from_dense_to_ptg(
@@ -338,6 +366,15 @@ def _forward(self, tensordict: TensorDictBase) -> TensorDictBase:
338366
tensordict.set(self.out_key, res)
339367
return tensordict
340368

369+
def _get_key_terminating_with(self, keys: List[NestedKey], key: str) -> NestedKey:
370+
for k in keys:
371+
k_tuple = _unravel_key_to_tuple(k)
372+
if k_tuple[-1] == key and self.agent_group in k_tuple:
373+
return k
374+
raise KeyError(
375+
f"Key terminating with {key} and containing {self.agent_group} not found in keys: {keys}"
376+
)
377+
341378

342379
def _get_edge_index(topology: str, self_loops: bool, n_agents: int, device: str):
343380
if topology == "full":
@@ -426,7 +463,9 @@ class GnnConfig(ModelConfig):
426463
gnn_kwargs: Optional[dict] = None
427464

428465
position_key: Optional[str] = None
466+
pos_features: Optional[int] = 0
429467
velocity_key: Optional[str] = None
468+
vel_features: Optional[int] = 0
430469
exclude_pos_from_node_features: Optional[bool] = None
431470
edge_radius: Optional[float] = None
432471

test/test_models.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -331,7 +331,7 @@ def test_gnn_edge_attrs(
331331
shape=multi_agent_obs.shape[len(batch_size) :]
332332
),
333333
"pos": UnboundedContinuousTensorSpec(
334-
shape=multi_agent_obs.shape[len(batch_size) :]
334+
shape=multi_agent_pos.shape[len(batch_size) :]
335335
),
336336
},
337337
shape=(n_agents,),
@@ -360,6 +360,7 @@ def test_gnn_edge_attrs(
360360
gnn_kwargs=None,
361361
position_key=position_key,
362362
exclude_pos_from_node_features=False,
363+
pos_features=pos_size if position_key is not None else 0,
363364
).get_model(
364365
input_spec=input_spec,
365366
output_spec=output_spec,
@@ -391,6 +392,7 @@ def test_gnn_edge_attrs(
391392
gnn_kwargs=None,
392393
position_key=position_key,
393394
exclude_pos_from_node_features=False,
395+
pos_features=pos_size if position_key is not None else 0,
394396
).get_model(
395397
input_spec=input_spec,
396398
output_spec=output_spec,

0 commit comments

Comments
 (0)