1111import warnings
1212from dataclasses import dataclass , MISSING
1313from math import prod
14- from typing import Optional , Type
14+ from typing import List , Optional , Type
1515
1616import torch
1717from tensordict import TensorDictBase
18- from tensordict .utils import _unravel_key_to_tuple
18+ from tensordict .utils import _unravel_key_to_tuple , NestedKey
1919from torch import nn , Tensor
2020
2121from 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
342379def _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
0 commit comments