From 96c9611ce945291a3a01b9e7502638d308de2431 Mon Sep 17 00:00:00 2001 From: Ashok Kumar Kannan Date: Mon, 16 Dec 2024 12:58:40 +0000 Subject: [PATCH] Trajectron++ model bringup --- .../multimodal/trajectron/test_trajectron.py | 307 ++++ .../trajectron/trajectron/__init__.py | 4 + .../trajectron/trajectron/argument_parser.py | 138 ++ .../trajectron/trajectron/config/config.json | 90 ++ .../trajectron/config/nuScenes.json | 109 ++ .../trajectron/dataset_envs/eth_val.pkl | Bin 0 -> 665952 bytes .../trajectron/environment/__init__.py | 11 + .../trajectron/environment/data_structures.py | 282 ++++ .../trajectron/environment/data_utils.py | 35 + .../trajectron/environment/environment.py | 66 + .../trajectron/trajectron/environment/map.py | 201 +++ .../trajectron/trajectron/environment/node.py | 256 ++++ .../trajectron/environment/node_type.py | 38 + .../trajectron/environment/scene.py | 218 +++ .../trajectron/environment/scene_graph.py | 536 +++++++ .../trajectron/evaluation/__init__.py | 4 + .../trajectron/evaluation/evaluation.py | 142 ++ .../trajectron/trajectron/model/__init__.py | 5 + .../trajectron/model/components/__init__.py | 7 + .../model/components/additive_attention.py | 71 + .../model/components/discrete_latent.py | 122 ++ .../trajectron/model/components/gmm2d.py | 187 +++ .../model/components/graph_attention.py | 61 + .../model/components/map_encoder.py | 33 + .../trajectron/model/dataset/__init__.py | 5 + .../trajectron/model/dataset/dataset.py | 95 ++ .../model/dataset/homography_warper.py | 467 +++++++ .../trajectron/model/dataset/preprocessing.py | 261 ++++ .../trajectron/model/dynamics/__init__.py | 7 + .../trajectron/model/dynamics/dynamic.py | 33 + .../trajectron/model/dynamics/linear.py | 15 + .../model/dynamics/single_integrator.py | 67 + .../trajectron/model/dynamics/unicycle.py | 239 ++++ .../trajectron/trajectron/model/mgcvae.py | 1240 +++++++++++++++++ .../trajectron/model/model_registrar.py | 76 + .../trajectron/model/model_utils.py | 137 ++ .../trajectron/model/online/__init__.py | 5 + .../trajectron/model/online/online_mgcvae.py | 428 ++++++ .../model/online/online_trajectron.py | 343 +++++ .../trajectron/trajectron/model/trajectron.py | 241 ++++ .../trajectron/model_dir/config.json | 1 + .../trajectron/trajectron/test/__init__.py | 0 .../trajectron/test/test_data_structures.py | 46 + .../trajectron/trajectron/test_online.py | 252 ++++ .../multimodal/trajectron/trajectron/train.py | 452 ++++++ .../trajectron/trajectron/utils/__init__.py | 6 + .../trajectron/utils/matrix_utils.py | 43 + .../trajectron/trajectron/utils/os_utils.py | 19 + .../trajectron/utils/trajectory_utils.py | 46 + .../trajectron/visualization/__init__.py | 5 + .../trajectron/visualization/visualization.py | 137 ++ .../visualization/visualization_utils.py | 30 + 52 files changed, 7619 insertions(+) create mode 100644 forge/test/models/pytorch/multimodal/trajectron/test_trajectron.py create mode 100644 forge/test/models/pytorch/multimodal/trajectron/trajectron/__init__.py create mode 100644 forge/test/models/pytorch/multimodal/trajectron/trajectron/argument_parser.py create mode 100644 forge/test/models/pytorch/multimodal/trajectron/trajectron/config/config.json create mode 100644 forge/test/models/pytorch/multimodal/trajectron/trajectron/config/nuScenes.json create mode 100644 forge/test/models/pytorch/multimodal/trajectron/trajectron/dataset_envs/eth_val.pkl create mode 100644 forge/test/models/pytorch/multimodal/trajectron/trajectron/environment/__init__.py create mode 100644 forge/test/models/pytorch/multimodal/trajectron/trajectron/environment/data_structures.py create mode 100644 forge/test/models/pytorch/multimodal/trajectron/trajectron/environment/data_utils.py create mode 100644 forge/test/models/pytorch/multimodal/trajectron/trajectron/environment/environment.py create mode 100644 forge/test/models/pytorch/multimodal/trajectron/trajectron/environment/map.py create mode 100644 forge/test/models/pytorch/multimodal/trajectron/trajectron/environment/node.py create mode 100644 forge/test/models/pytorch/multimodal/trajectron/trajectron/environment/node_type.py create mode 100644 forge/test/models/pytorch/multimodal/trajectron/trajectron/environment/scene.py create mode 100644 forge/test/models/pytorch/multimodal/trajectron/trajectron/environment/scene_graph.py create mode 100644 forge/test/models/pytorch/multimodal/trajectron/trajectron/evaluation/__init__.py create mode 100644 forge/test/models/pytorch/multimodal/trajectron/trajectron/evaluation/evaluation.py create mode 100644 forge/test/models/pytorch/multimodal/trajectron/trajectron/model/__init__.py create mode 100644 forge/test/models/pytorch/multimodal/trajectron/trajectron/model/components/__init__.py create mode 100644 forge/test/models/pytorch/multimodal/trajectron/trajectron/model/components/additive_attention.py create mode 100644 forge/test/models/pytorch/multimodal/trajectron/trajectron/model/components/discrete_latent.py create mode 100644 forge/test/models/pytorch/multimodal/trajectron/trajectron/model/components/gmm2d.py create mode 100644 forge/test/models/pytorch/multimodal/trajectron/trajectron/model/components/graph_attention.py create mode 100644 forge/test/models/pytorch/multimodal/trajectron/trajectron/model/components/map_encoder.py create mode 100644 forge/test/models/pytorch/multimodal/trajectron/trajectron/model/dataset/__init__.py create mode 100644 forge/test/models/pytorch/multimodal/trajectron/trajectron/model/dataset/dataset.py create mode 100644 forge/test/models/pytorch/multimodal/trajectron/trajectron/model/dataset/homography_warper.py create mode 100644 forge/test/models/pytorch/multimodal/trajectron/trajectron/model/dataset/preprocessing.py create mode 100644 forge/test/models/pytorch/multimodal/trajectron/trajectron/model/dynamics/__init__.py create mode 100644 forge/test/models/pytorch/multimodal/trajectron/trajectron/model/dynamics/dynamic.py create mode 100644 forge/test/models/pytorch/multimodal/trajectron/trajectron/model/dynamics/linear.py create mode 100644 forge/test/models/pytorch/multimodal/trajectron/trajectron/model/dynamics/single_integrator.py create mode 100644 forge/test/models/pytorch/multimodal/trajectron/trajectron/model/dynamics/unicycle.py create mode 100644 forge/test/models/pytorch/multimodal/trajectron/trajectron/model/mgcvae.py create mode 100644 forge/test/models/pytorch/multimodal/trajectron/trajectron/model/model_registrar.py create mode 100644 forge/test/models/pytorch/multimodal/trajectron/trajectron/model/model_utils.py create mode 100644 forge/test/models/pytorch/multimodal/trajectron/trajectron/model/online/__init__.py create mode 100644 forge/test/models/pytorch/multimodal/trajectron/trajectron/model/online/online_mgcvae.py create mode 100644 forge/test/models/pytorch/multimodal/trajectron/trajectron/model/online/online_trajectron.py create mode 100644 forge/test/models/pytorch/multimodal/trajectron/trajectron/model/trajectron.py create mode 100644 forge/test/models/pytorch/multimodal/trajectron/trajectron/model_dir/config.json create mode 100644 forge/test/models/pytorch/multimodal/trajectron/trajectron/test/__init__.py create mode 100644 forge/test/models/pytorch/multimodal/trajectron/trajectron/test/test_data_structures.py create mode 100644 forge/test/models/pytorch/multimodal/trajectron/trajectron/test_online.py create mode 100644 forge/test/models/pytorch/multimodal/trajectron/trajectron/train.py create mode 100644 forge/test/models/pytorch/multimodal/trajectron/trajectron/utils/__init__.py create mode 100644 forge/test/models/pytorch/multimodal/trajectron/trajectron/utils/matrix_utils.py create mode 100644 forge/test/models/pytorch/multimodal/trajectron/trajectron/utils/os_utils.py create mode 100644 forge/test/models/pytorch/multimodal/trajectron/trajectron/utils/trajectory_utils.py create mode 100644 forge/test/models/pytorch/multimodal/trajectron/trajectron/visualization/__init__.py create mode 100644 forge/test/models/pytorch/multimodal/trajectron/trajectron/visualization/visualization.py create mode 100644 forge/test/models/pytorch/multimodal/trajectron/trajectron/visualization/visualization_utils.py diff --git a/forge/test/models/pytorch/multimodal/trajectron/test_trajectron.py b/forge/test/models/pytorch/multimodal/trajectron/test_trajectron.py new file mode 100644 index 000000000..c675387be --- /dev/null +++ b/forge/test/models/pytorch/multimodal/trajectron/test_trajectron.py @@ -0,0 +1,307 @@ +# # SPDX-FileCopyrightText: © 2024 Tenstorrent AI ULC + +# # SPDX-License-Identifier: Apache-2.0 + +import sys + +sys.path.append("forge/test/models/pytorch/multimodal/trajectron/trajectron/") +import pytest +import forge +from test.models.pytorch.multimodal.trajectron.trajectron.model import Trajectron +from test.models.pytorch.multimodal.trajectron.trajectron.model.model_registrar import ModelRegistrar +from test.models.pytorch.multimodal.trajectron.trajectron.model.dataset import ( + EnvironmentDataset, + collate, + get_timesteps_data, +) +from forge.verify.compare import compare_with_golden +import os +import json +import dill +import torch +import torch.nn as nn +import numpy as np +from typing import Any +import torch.nn.utils.rnn as rnn +import pytest + + +def load_hyperparams(): + conf_path = "forge/test/models/pytorch/multimodal/trajectron/trajectron/config/config.json" + with open(conf_path, "r", encoding="utf-8") as conf_json: + hyperparams = json.load(conf_json) + + # Set Default values + hyperparams["scene_freq_mult_eval"] = False + hyperparams["node_freq_mult_eval"] = False + hyperparams["edge_encoding"] = False + hyperparams["incl_robot_node"] = False + hyperparams["use_map_encoding"] = False + + hyperparams["edge_addition_filter"] = [1, 1] + hyperparams["edge_removal_filter"] = [1, 1] + + return hyperparams + + +def load_env(): + eval_data_path = "forge/test/models/pytorch/multimodal/trajectron/trajectron/dataset_envs/eth_val.pkl" + with open(eval_data_path, "rb") as f: + eval_env = dill.load(f, encoding="latin1") + return eval_env + + +class TrajectronWrapper(nn.Module): + def __init__( + self, + model_dir: str, + hyperparams: dict[str, Any], + env: Any, + scene_index: int, + num_samples: int = 1, + z_mode: bool = True, + gmm_mode: bool = True, + all_z_sep: bool = False, + full_dist: bool = False, + ): + super().__init__() + + # Build Model registrar + if not os.path.exists(model_dir): + os.makedirs(model_dir, exist_ok=False) + model_config_path = model_dir + "/config.json" + if not os.path.exists(model_config_path): + with open(model_config_path, "w") as conf_json: + json.dump(hyperparams, conf_json) + model_registrar = ModelRegistrar(model_dir, "cpu") + + # Build Trajectron Model + self.model = Trajectron(model_registrar=model_registrar, hyperparams=hyperparams, log_writer=None, device="cpu") + self.model.set_environment(env=env) + + self.model_dir = model_dir + self.hyperparams = hyperparams + self.env = env + + assert len(self.env.NodeType) == 1 + self.node_type = self.env.NodeType[0] + + self.scene_index = scene_index + self.num_samples = num_samples + self.z_mode = z_mode + self.gmm_mode = gmm_mode + self.all_z_sep = all_z_sep + self.full_dist = full_dist + + def _build_packed_sequence( + self, + packed_sequence_data, + packed_sequence_batch_sizes, + packed_sequence_sorted_indices, + packed_sequence_unsorted_indices, + ): + packed_sequence = torch.nn.utils.rnn.PackedSequence( + data=packed_sequence_data.squeeze(), + batch_sizes=packed_sequence_batch_sizes.squeeze(), + sorted_indices=packed_sequence_sorted_indices.squeeze(), + unsorted_indices=packed_sequence_unsorted_indices.squeeze(), + ) + return packed_sequence + + def forward( + self, + x, + x_st_t, + packed_sequence_data, + packed_sequence_batch_sizes, + packed_sequence_sorted_indices, + packed_sequence_unsorted_indices, + first_history_index, + ): + neighbors_data_st = None + neighbors_edge_value = None + robot_traj_st_t = None + map = None + + ph = self.hyperparams["prediction_horizon"] + + packed_x_st_t = self._build_packed_sequence( + packed_sequence_data, + packed_sequence_batch_sizes, + packed_sequence_sorted_indices, + packed_sequence_unsorted_indices, + ) + + model = self.model.node_models_dict[self.node_type] + predictions = model.predict( + inputs=x, + inputs_st=x_st_t, # Pack and send this + packed_inputs_st=packed_x_st_t, + first_history_indices=first_history_index, + neighbors=neighbors_data_st, + neighbors_edge_value=neighbors_edge_value, + robot=robot_traj_st_t, + map=map, + prediction_horizon=ph, + num_samples=self.num_samples, + z_mode=self.z_mode, + gmm_mode=self.gmm_mode, + full_dist=self.full_dist, + all_z_sep=self.all_z_sep, + ) + + return predictions + + def eval(self): + super().eval() + self.model.eval() + + def get_input_batch(self, scene): + ph = self.hyperparams["prediction_horizon"] + timesteps = scene.sample_timesteps(1, min_future_timesteps=ph) + + min_future_timesteps = ph + min_history_timesteps = 1 + + node_type = self.node_type + assert node_type in self.model.pred_state + model = self.model.node_models_dict[node_type] + + # Get Input data for node type and given timesteps + batch = get_timesteps_data( + env=self.env, + scene=scene, + t=timesteps, + node_type=node_type, + state=self.model.state, + pred_state=self.model.pred_state, + edge_types=model.edge_types, + min_ht=min_history_timesteps, + max_ht=self.model.max_ht, + min_ft=min_future_timesteps, + max_ft=min_future_timesteps, + hyperparams=self.hyperparams, + ) + + assert batch is not None + + ( + ( + first_history_index, + x_t, + y_t, + x_st_t, + y_st_t, + neighbors_data_st, + neighbors_edge_value, + robot_traj_st_t, + map, + ), + nodes, + timesteps_o, + ) = batch + + device = self.model.device + x = x_t.to(device) + x_st_t = x_st_t.to(device) + if robot_traj_st_t is not None: + robot_traj_st_t = robot_traj_st_t.to(device) + + if type(map) == torch.Tensor: + map = map.to(device) + + return (x, x_st_t, first_history_index, neighbors_data_st, neighbors_edge_value, robot_traj_st_t, map), ( + nodes, + timesteps_o, + ) + + +def pack_input_sequences(sequences, lower_indices=None, upper_indices=None, total_length=None): + bs, tf = sequences.shape[:2] + if lower_indices is None: + lower_indices = torch.zeros(bs, dtype=torch.int) + if upper_indices is None: + upper_indices = torch.ones(bs, dtype=torch.int) * (tf - 1) + if total_length is None: + total_length = max(upper_indices) + 1 + # This is done so that we can just pass in self.prediction_timesteps + # (which we want to INCLUDE, so this will exclude the next timestep). + inclusive_break_indices = upper_indices + 1 + + pad_list = list() + for i, seq_len in enumerate(inclusive_break_indices): + pad_list.append(sequences[i, lower_indices[i] : seq_len]) + + packed_seqs = rnn.pack_sequence(pad_list, enforce_sorted=False) + + return packed_seqs + + +def get_packed_sequence_values(packed_sequence): + values = ( + packed_sequence.data.unsqueeze(0).unsqueeze(0), + packed_sequence.batch_sizes.unsqueeze(0), + packed_sequence.sorted_indices.unsqueeze(0), + packed_sequence.unsorted_indices.unsqueeze(0), + ) + return values + + +@pytest.mark.nightly +@pytest.mark.model_analysis +def test_trajectronpp_pytorch(): + env = load_env() + hyperparams = load_hyperparams() + model_dir = "forge/test/models/pytorch/multimodal/trajectron/trajectron/model_dir" + + # Build Pytorch Model + pt_model = TrajectronWrapper(model_dir=model_dir, hyperparams=hyperparams, env=env, scene_index=0) + pt_model.eval() + + scene = env.scenes[0] + inputs_batch = pt_model.get_input_batch(scene=scene) + + (x, x_st_t, first_history_index, neighbors_data_st, neighbors_edge_value, robot_traj_st_t, map), ( + nodes, + timesteps_o, + ) = inputs_batch + + packed_x_st_t = pack_input_sequences(x_st_t, lower_indices=first_history_index) + ( + packed_sequence_data, + packed_sequence_batch_sizes, + packed_sequence_sorted_indices, + packed_sequence_unsorted_indices, + ) = get_packed_sequence_values(packed_x_st_t) + + assert neighbors_data_st is None + assert neighbors_edge_value is None + assert robot_traj_st_t is None + assert map is None + # Run CPU Inference + output = pt_model( + x, + x_st_t, + packed_sequence_data, + packed_sequence_batch_sizes, + packed_sequence_sorted_indices, + packed_sequence_unsorted_indices, + first_history_index, + ) + inputs = [ + x, + x_st_t, + packed_sequence_data, + packed_sequence_batch_sizes, + packed_sequence_sorted_indices, + packed_sequence_unsorted_indices, + first_history_index, + ] + compiled_model = forge.compile(pt_model, inputs) + co_out = compiled_model(*inputs) + fw_out = pt_model(*inputs) + + co_out = [co.to("cpu") for co in co_out] + fw_out = [fw_out] if isinstance(fw_out, torch.Tensor) else fw_out + + assert all([compare_with_golden(golden=fo, calculated=co, pcc=0.99) for fo, co in zip(fw_out, co_out)]) diff --git a/forge/test/models/pytorch/multimodal/trajectron/trajectron/__init__.py b/forge/test/models/pytorch/multimodal/trajectron/trajectron/__init__.py new file mode 100644 index 000000000..e7543593d --- /dev/null +++ b/forge/test/models/pytorch/multimodal/trajectron/trajectron/__init__.py @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: (c) 2024 Tenstorrent AI ULC +# +# SPDX-License-Identifier: Apache-2.0 +from model import Trajectron diff --git a/forge/test/models/pytorch/multimodal/trajectron/trajectron/argument_parser.py b/forge/test/models/pytorch/multimodal/trajectron/trajectron/argument_parser.py new file mode 100644 index 000000000..526e95fb3 --- /dev/null +++ b/forge/test/models/pytorch/multimodal/trajectron/trajectron/argument_parser.py @@ -0,0 +1,138 @@ +# SPDX-FileCopyrightText: (c) 2024 Tenstorrent AI ULC +# +# SPDX-License-Identifier: Apache-2.0 +import argparse + +parser = argparse.ArgumentParser() +parser.add_argument( + "--conf", help="path to json config file for hyperparameters", type=str, default="../config/config.json" +) + +parser.add_argument("--debug", help="disable all disk writing processes.", action="store_true") + +parser.add_argument("--preprocess_workers", help="number of processes to spawn for preprocessing", type=int, default=0) + + +# Model Parameters +parser.add_argument( + "--offline_scene_graph", + help="whether to precompute the scene graphs offline, options are 'no' and 'yes'", + type=str, + default="yes", +) + +parser.add_argument( + "--dynamic_edges", help="whether to use dynamic edges or not, options are 'no' and 'yes'", type=str, default="yes" +) + +parser.add_argument( + "--edge_state_combine_method", + help="the method to use for combining edges of the same type", + type=str, + default="sum", +) + +parser.add_argument( + "--edge_influence_combine_method", + help="the method to use for combining edge influences", + type=str, + default="attention", +) + +parser.add_argument( + "--edge_addition_filter", + nargs="+", + help="what scaling to use for edges as they're created", + type=float, + default=[0.25, 0.5, 0.75, 1.0], +) # We don't automatically pad left with 0.0, if you want a sharp +# and short edge addition, then you need to have a 0.0 at the +# beginning, e.g. [0.0, 1.0]. + +parser.add_argument( + "--edge_removal_filter", + nargs="+", + help="what scaling to use for edges as they're removed", + type=float, + default=[1.0, 0.0], +) # We don't automatically pad right with 0.0, if you want a sharp drop off like +# the default, then you need to have a 0.0 at the end. + +parser.add_argument( + "--override_attention_radius", + action="append", + help='Specify one attention radius to override. E.g. "PEDESTRIAN VEHICLE 10.0"', + default=[], +) + +parser.add_argument( + "--incl_robot_node", + help="whether to include a robot node in the graph or simply model all agents", + action="store_true", +) + +parser.add_argument("--map_encoding", help="Whether to use map encoding or not", action="store_true") + +parser.add_argument("--augment", help="Whether to augment the scene during training", action="store_true") + +parser.add_argument( + "--node_freq_mult_train", help="Whether to use frequency multiplying of nodes during training", action="store_true" +) + +parser.add_argument( + "--node_freq_mult_eval", help="Whether to use frequency multiplying of nodes during evaluation", action="store_true" +) + +parser.add_argument( + "--scene_freq_mult_train", help="Whether to use frequency multiplying of nodes during training", action="store_true" +) + +parser.add_argument( + "--scene_freq_mult_eval", + help="Whether to use frequency multiplying of nodes during evaluation", + action="store_true", +) + +parser.add_argument( + "--scene_freq_mult_viz", help="Whether to use frequency multiplying of nodes during evaluation", action="store_true" +) + +parser.add_argument("--no_edge_encoding", help="Whether to use neighbors edge encoding", action="store_true") + +# Data Parameters +parser.add_argument("--data_dir", help="what dir to look in for data", type=str, default="../experiments/processed") + +parser.add_argument("--train_data_dict", help="what file to load for training data", type=str, default="train.pkl") + +parser.add_argument("--eval_data_dict", help="what file to load for evaluation data", type=str, default="val.pkl") + +parser.add_argument( + "--log_dir", + help="what dir to save training information (i.e., saved models, logs, etc)", + type=str, + default="../experiments/logs", +) + +parser.add_argument("--log_tag", help="tag for the log folder", type=str, default="") + +parser.add_argument("--device", help="what device to perform training on", type=str, default="cuda:0") + +parser.add_argument("--eval_device", help="what device to use during evaluation", type=str, default=None) + +# Training Parameters +parser.add_argument("--train_epochs", help="number of iterations to train for", type=int, default=1) + +parser.add_argument("--batch_size", help="training batch size", type=int, default=256) + +parser.add_argument("--eval_batch_size", help="evaluation batch size", type=int, default=256) + +parser.add_argument("--k_eval", help="how many samples to take during evaluation", type=int, default=25) + +parser.add_argument("--seed", help="manual seed to use, default is 123", type=int, default=123) + +parser.add_argument("--eval_every", help="how often to evaluate during training, never if None", type=int, default=1) + +parser.add_argument("--vis_every", help="how often to visualize during training, never if None", type=int, default=1) + +parser.add_argument("--save_every", help="how often to save during training, never if None", type=int, default=1) +args = parser.parse_args() diff --git a/forge/test/models/pytorch/multimodal/trajectron/trajectron/config/config.json b/forge/test/models/pytorch/multimodal/trajectron/trajectron/config/config.json new file mode 100644 index 000000000..fca815729 --- /dev/null +++ b/forge/test/models/pytorch/multimodal/trajectron/trajectron/config/config.json @@ -0,0 +1,90 @@ +{ + + "batch_size": 256, + "grad_clip": 1.0, + + "learning_rate_style": "exp", + "learning_rate": 0.001, + "min_learning_rate": 0.00001, + "learning_decay_rate": 0.9999, + + "prediction_horizon": 12, + "minimum_history_length": 1, + "maximum_history_length": 8, + + "map_encoder": { + "PEDESTRIAN": { + "heading_state_index": 5, + "patch_size": [50, 10, 50, 90], + "map_channels": 3, + "hidden_channels": [10, 20, 10, 1], + "output_size": 32, + "masks": [5, 5, 5, 5], + "strides": [1, 1, 1, 1], + "dropout": 0.5 + } + }, + + "k": 1, + "k_eval": 1, + + "kl_min": 0.07, + "kl_weight": 100.0, + "kl_weight_start": 0, + "kl_decay_rate": 0.99995, + "kl_crossover": 400, + "kl_sigmoid_divisor": 4, + + "rnn_kwargs": { + "dropout_keep_prob": 0.75 + }, + "MLP_dropout_keep_prob": 0.9, + "enc_rnn_dim_edge": 32, + "enc_rnn_dim_edge_influence": 32, + "enc_rnn_dim_history": 32, + "enc_rnn_dim_future": 32, + "dec_rnn_dim": 128, + + "q_z_xy_MLP_dims": null, + "p_z_x_MLP_dims": 32, + "GMM_components": 1, + + "log_p_yt_xz_max": 6, + + "N": 1, + "K": 25, + + "tau_init": 2.0, + "tau_final": 0.05, + "tau_decay_rate": 0.997, + + "use_z_logit_clipping": true, + "z_logit_clip_start": 0.05, + "z_logit_clip_final": 5.0, + "z_logit_clip_crossover": 300, + "z_logit_clip_divisor": 5, + + "dynamic": { + "PEDESTRIAN": { + "name": "SingleIntegrator", + "distribution": true, + "limits": {} + } + }, + + "state": { + "PEDESTRIAN": { + "position": ["x", "y"], + "velocity": ["x", "y"], + "acceleration": ["x", "y"] + } + }, + + "pred_state": { + "PEDESTRIAN": { + "position": ["x", "y"] + } + }, + + "log_histograms": false +} diff --git a/forge/test/models/pytorch/multimodal/trajectron/trajectron/config/nuScenes.json b/forge/test/models/pytorch/multimodal/trajectron/trajectron/config/nuScenes.json new file mode 100644 index 000000000..acebf8e1e --- /dev/null +++ b/forge/test/models/pytorch/multimodal/trajectron/trajectron/config/nuScenes.json @@ -0,0 +1,109 @@ +{ + + "batch_size": 256, + "grad_clip": 1.0, + + "learning_rate_style": "exp", + "learning_rate": 0.003, + "min_learning_rate": 0.00001, + "learning_decay_rate": 0.9999, + + "prediction_horizon": 6, + "minimum_history_length": 1, + "maximum_history_length": 8, + + "map_encoder": { + "VEHICLE": { + "heading_state_index": 6, + "patch_size": [50, 10, 50, 90], + "map_channels": 3, + "hidden_channels": [10, 20, 10, 1], + "output_size": 32, + "masks": [5, 5, 5, 3], + "strides": [2, 2, 1, 1], + "dropout": 0.5 + } + }, + + "k": 1, + "k_eval": 1, + + "kl_min": 0.07, + "kl_weight": 100.0, + "kl_weight_start": 0, + "kl_decay_rate": 0.99995, + "kl_crossover": 400, + "kl_sigmoid_divisor": 4, + + "rnn_kwargs": { + "dropout_keep_prob": 0.75 + }, + "MLP_dropout_keep_prob": 0.9, + "enc_rnn_dim_edge": 32, + "enc_rnn_dim_edge_influence": 32, + "enc_rnn_dim_history": 32, + "enc_rnn_dim_future": 32, + "dec_rnn_dim": 128, + + "q_z_xy_MLP_dims": null, + "p_z_x_MLP_dims": 32, + "GMM_components": 1, + + "log_p_yt_xz_max": 6, + + "N": 1, + "K": 25, + + "tau_init": 2.0, + "tau_final": 0.05, + "tau_decay_rate": 0.997, + + "use_z_logit_clipping": true, + "z_logit_clip_start": 0.05, + "z_logit_clip_final": 5.0, + "z_logit_clip_crossover": 300, + "z_logit_clip_divisor": 5, + + "dynamic": { + "PEDESTRIAN": { + "name": "SingleIntegrator", + "distribution": true, + "limits": {} + }, + "VEHICLE": { + "name": "Unicycle", + "distribution": true, + "limits": { + "max_a": 4, + "min_a": -5, + "max_heading_change": 0.7, + "min_heading_change": -0.7 + } + } + }, + + "state": { + "PEDESTRIAN": { + "position": ["x", "y"], + "velocity": ["x", "y"], + "acceleration": ["x", "y"] + }, + "VEHICLE": { + "position": ["x", "y"], + "velocity": ["x", "y"], + "acceleration": ["x", "y"], + "heading": ["°", "d°"] + } + }, + + "pred_state": { + "VEHICLE": { + "position": ["x", "y"] + }, + "PEDESTRIAN": { + "position": ["x", "y"] + } + }, + + "log_histograms": false +} diff --git a/forge/test/models/pytorch/multimodal/trajectron/trajectron/dataset_envs/eth_val.pkl b/forge/test/models/pytorch/multimodal/trajectron/trajectron/dataset_envs/eth_val.pkl new file mode 100644 index 0000000000000000000000000000000000000000..8afc0d7c0806593b4f508bbd7f0830597b9690e1 GIT binary patch literal 665952 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
Ih%uh92LE0cagSv|!=`tUu?+%;hEy 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

&ENm@IN17F$&TxPYM$77rRD$DePY|a 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 MpPk;Z=nFOZ52w`fV*mgE literal 0 HcmV?d00001 diff --git a/forge/test/models/pytorch/multimodal/trajectron/trajectron/environment/__init__.py b/forge/test/models/pytorch/multimodal/trajectron/trajectron/environment/__init__.py new file mode 100644 index 000000000..422f33fc3 --- /dev/null +++ b/forge/test/models/pytorch/multimodal/trajectron/trajectron/environment/__init__.py @@ -0,0 +1,11 @@ +# SPDX-FileCopyrightText: (c) 2024 Tenstorrent AI ULC +# +# SPDX-License-Identifier: Apache-2.0 +from .data_structures import RingBuffer, SingleHeaderNumpyArray, DoubleHeaderNumpyArray +from .scene import Scene +from .node import Node +from .scene_graph import TemporalSceneGraph, SceneGraph +from .environment import Environment +from .node_type import NodeTypeEnum +from .data_utils import derivative_of +from .map import GeometricMap diff --git a/forge/test/models/pytorch/multimodal/trajectron/trajectron/environment/data_structures.py b/forge/test/models/pytorch/multimodal/trajectron/trajectron/environment/data_structures.py new file mode 100644 index 000000000..20e50e83e --- /dev/null +++ b/forge/test/models/pytorch/multimodal/trajectron/trajectron/environment/data_structures.py @@ -0,0 +1,282 @@ +# SPDX-FileCopyrightText: (c) 2024 Tenstorrent AI ULC +# +# SPDX-License-Identifier: Apache-2.0 +import numpy as np +import pandas as pd +from collections.abc import Sequence +from collections import OrderedDict + + +class RingBuffer(Sequence): + def __init__(self, capacity, dtype=float, allow_overwrite=True): + """ + Create a new ring buffer with the given capacity and element type. + Code copy-pasted from: https://github.com/eric-wieser/numpy_ringbuffer + + Parameters + ---------- + capacity: int + The maximum capacity of the ring buffer + dtype: data-type, optional + Desired type of buffer elements. Use a type like (float, 2) to + produce a buffer with shape (N, 2) + allow_overwrite: bool + If false, throw an IndexError when trying to append to an already + full buffer + """ + self._arr = np.full(capacity, np.nan, dtype) + self._left_index = 0 + self._right_index = 0 + self._capacity = capacity + self._allow_overwrite = allow_overwrite + + def _unwrap(self): + """Copy the data from this buffer into unwrapped form""" + return np.concatenate( + ( + self._arr[self._left_index : min(self._right_index, self._capacity)], + self._arr[: max(self._right_index - self._capacity, 0)], + ) + ) + + def _fix_indices(self): + """ + Enforce our invariant that 0 <= self._left_index < self._capacity + """ + if self._left_index >= self._capacity: + self._left_index -= self._capacity + self._right_index -= self._capacity + elif self._left_index < 0: + self._left_index += self._capacity + self._right_index += self._capacity + + @property + def is_full(self): + """True if there is no more space in the buffer""" + return len(self) == self._capacity + + # numpy compatibility + def __array__(self): + return self._unwrap() + + @property + def dtype(self): + return self._arr.dtype + + @property + def shape(self): + return (len(self),) + self._arr.shape[1:] + + # these mirror methods from deque + @property + def maxlen(self): + return self._capacity + + def append(self, value): + if self.is_full: + if not self._allow_overwrite: + raise IndexError("append to a full RingBuffer with overwrite disabled") + elif not len(self): + return + else: + self._left_index += 1 + + self._arr[self._right_index % self._capacity] = value + self._right_index += 1 + self._fix_indices() + + def appendleft(self, value): + if self.is_full: + if not self._allow_overwrite: + raise IndexError("append to a full RingBuffer with overwrite disabled") + elif not len(self): + return + else: + self._right_index -= 1 + + self._left_index -= 1 + self._fix_indices() + self._arr[self._left_index] = value + + def pop(self): + if len(self) == 0: + raise IndexError("pop from an empty RingBuffer") + self._right_index -= 1 + self._fix_indices() + res = self._arr[self._right_index % self._capacity] + return res + + def popleft(self): + if len(self) == 0: + raise IndexError("pop from an empty RingBuffer") + res = self._arr[self._left_index] + self._left_index += 1 + self._fix_indices() + return res + + def extend(self, values): + lv = len(values) + if len(self) + lv > self._capacity: + if not self._allow_overwrite: + raise IndexError("extend a RingBuffer such that it would overflow, with overwrite disabled") + elif not len(self): + return + if lv >= self._capacity: + # wipe the entire array! - this may not be threadsafe + self._arr[...] = values[-self._capacity :] + self._right_index = self._capacity + self._left_index = 0 + return + + ri = self._right_index % self._capacity + sl1 = np.s_[ri : min(ri + lv, self._capacity)] + sl2 = np.s_[: max(ri + lv - self._capacity, 0)] + self._arr[sl1] = values[: sl1.stop - sl1.start] + self._arr[sl2] = values[sl1.stop - sl1.start :] + self._right_index += lv + + self._left_index = max(self._left_index, self._right_index - self._capacity) + self._fix_indices() + + def extendleft(self, values): + lv = len(values) + if len(self) + lv > self._capacity: + if not self._allow_overwrite: + raise IndexError("extend a RingBuffer such that it would overflow, with overwrite disabled") + elif not len(self): + return + if lv >= self._capacity: + # wipe the entire array! - this may not be threadsafe + self._arr[...] = values[: self._capacity] + self._right_index = self._capacity + self._left_index = 0 + return + + self._left_index -= lv + self._fix_indices() + li = self._left_index + sl1 = np.s_[li : min(li + lv, self._capacity)] + sl2 = np.s_[: max(li + lv - self._capacity, 0)] + self._arr[sl1] = values[: sl1.stop - sl1.start] + self._arr[sl2] = values[sl1.stop - sl1.start :] + + self._right_index = min(self._right_index, self._left_index + self._capacity) + + # implement Sequence methods + def __len__(self): + return self._right_index - self._left_index + + def __getitem__(self, item): + # handle simple (b[1]) and basic (b[np.array([1, 2, 3])]) fancy indexing specially + if not isinstance(item, tuple): + item_arr = np.asarray(item) + if issubclass(item_arr.dtype.type, np.integer): + item_arr = (item_arr + self._left_index) % self._capacity + return self._arr[item_arr] + + # for everything else, get it right at the expense of efficiency + return self._unwrap()[item] + + def __iter__(self): + # alarmingly, this is comparable in speed to using itertools.chain + return iter(self._unwrap()) + + # Everything else + def __repr__(self): + return "".format(np.asarray(self)) + + +class DoubleHeaderNumpyArray(object): + def __init__(self, data: np.ndarray, header: list): + """ + Data Structure mirroring some functionality of double indexed pandas DataFrames. + Indexing options are: + [:, (header1, header2)] + [:, [(header1, header2), (header1, header2)]] + [:, {header1: [header21, header22]}] + + A SingleHeaderNumpyArray can is returned if an element of the first header is querried as attribut: + doubleHeaderNumpyArray.position -> SingleHeaderNumpyArray + + :param data: The numpy array. + :param header: The double header structure as list of tuples [(header11, header21), (header11, header22) ...] + """ + self.data = data + self.header = header + self.double_header_lookup = OrderedDict() + self.tree_header_lookup = OrderedDict() + for i, header_item in enumerate(header): + self.double_header_lookup[header_item] = i + if header_item[0] not in self.tree_header_lookup: + self.tree_header_lookup[header_item[0]] = dict() + self.tree_header_lookup[header_item[0]][header_item[1]] = i + + def __mul__(self, other): + return DoubleHeaderNumpyArray(self.data * other, self.header) + + def get_single_header_array(self, h1: str, rows=slice(None, None, None)): + data_integer_indices = list() + h2_list = list() + for h2 in self.tree_header_lookup[h1]: + data_integer_indices.append(self.tree_header_lookup[h1][h2]) + h2_list.append(h2) + return SingleHeaderNumpyArray(self.data[rows, data_integer_indices], h2_list) + + def __getitem__(self, item): + rows, columns = item + data_integer_indices = list() + if type(columns) is dict: + for h1, h2s in columns.items(): + for h2 in h2s: + data_integer_indices.append(self.double_header_lookup[(h1, h2)]) + return self.data[rows, data_integer_indices] + elif type(columns) is list: + for column in columns: + assert type(column) is tuple, "If Index is list it hast to be list of double header tuples." + data_integer_indices.append(self.double_header_lookup[column]) + return self.data[rows, data_integer_indices] + elif type(columns) is tuple: + return self.data[rows, self.double_header_lookup[columns]] + else: + assert type(item) is str, "Index must be str, list of tuples or dict of tree structure." + return self.get_single_header_array(item, rows=rows) + + def __getattr__(self, item): + if not item.startswith("_"): + if item in self.tree_header_lookup.keys(): + return self.get_single_header_array(item) + else: + try: + return self.data.__getattribute__(item) + except AttributeError: + return super().__getattribute__(item) + else: + return super().__getattribute__(item) + + +class SingleHeaderNumpyArray(object): + def __init__(self, data: np.ndarray, header: list): + self.data = data + self.header_lookup = OrderedDict({h: i for i, h in enumerate(header)}) + + def __getitem__(self, item): + rows, columns = item + data_integer_indices = list() + if type(columns) is list or type(columns) is tuple: + for column in columns: + data_integer_indices.append(self.header_lookup[column]) + else: + data_integer_indices = self.header_lookup[columns] + return self.data[rows, data_integer_indices] + + def __getattr__(self, item): + if not item.startswith("_"): + if item in self.header_lookup.keys(): + return self[:, item] + else: + try: + return self.data.__getattribute__(item) + except AttributeError: + return super().__getattribute__(item) + else: + return super().__getattribute__(item) diff --git a/forge/test/models/pytorch/multimodal/trajectron/trajectron/environment/data_utils.py b/forge/test/models/pytorch/multimodal/trajectron/trajectron/environment/data_utils.py new file mode 100644 index 000000000..f8c5d1ff9 --- /dev/null +++ b/forge/test/models/pytorch/multimodal/trajectron/trajectron/environment/data_utils.py @@ -0,0 +1,35 @@ +# SPDX-FileCopyrightText: (c) 2024 Tenstorrent AI ULC +# +# SPDX-License-Identifier: Apache-2.0 +import numpy as np + + +def make_continuous_copy(alpha): + alpha = (alpha + np.pi) % (2.0 * np.pi) - np.pi + continuous_x = np.zeros_like(alpha) + continuous_x[0] = alpha[0] + for i in range(1, len(alpha)): + if not (np.sign(alpha[i]) == np.sign(alpha[i - 1])) and np.abs(alpha[i]) > np.pi / 2: + continuous_x[i] = ( + continuous_x[i - 1] + (alpha[i] - alpha[i - 1]) - np.sign((alpha[i] - alpha[i - 1])) * 2 * np.pi + ) + else: + continuous_x[i] = continuous_x[i - 1] + (alpha[i] - alpha[i - 1]) + + return continuous_x + + +def derivative_of(x, dt=1, radian=False): + if radian: + x = make_continuous_copy(x) + + not_nan_mask = ~np.isnan(x) + masked_x = x[not_nan_mask] + + if masked_x.shape[-1] < 2: + return np.zeros_like(x) + + dx = np.full_like(x, np.nan) + dx[not_nan_mask] = np.ediff1d(masked_x, to_begin=(masked_x[1] - masked_x[0])) / dt + + return dx diff --git a/forge/test/models/pytorch/multimodal/trajectron/trajectron/environment/environment.py b/forge/test/models/pytorch/multimodal/trajectron/trajectron/environment/environment.py new file mode 100644 index 000000000..48bf80d4f --- /dev/null +++ b/forge/test/models/pytorch/multimodal/trajectron/trajectron/environment/environment.py @@ -0,0 +1,66 @@ +# SPDX-FileCopyrightText: (c) 2024 Tenstorrent AI ULC +# +# SPDX-License-Identifier: Apache-2.0 +import json +import numpy as np +from itertools import product +from .node_type import NodeTypeEnum + + +class Environment(object): + def __init__(self, node_type_list, standardization, scenes=None, attention_radius=None, robot_type=None): + self.scenes = scenes + self.node_type_list = node_type_list + self.attention_radius = attention_radius + self.NodeType = NodeTypeEnum(node_type_list) + self.robot_type = robot_type + + self.standardization = standardization + self.standardize_param_memo = dict() + + self._scenes_resample_prop = None + + def get_edge_types(self): + return list(product(self.NodeType, repeat=2)) + + def get_standardize_params(self, state, node_type): + memo_key = (json.dumps(state), node_type) + if memo_key in self.standardize_param_memo: + return self.standardize_param_memo[memo_key] + + standardize_mean_list = list() + standardize_std_list = list() + for entity, dims in state.items(): + for dim in dims: + standardize_mean_list.append(self.standardization[node_type][entity][dim]["mean"]) + standardize_std_list.append(self.standardization[node_type][entity][dim]["std"]) + standardize_mean = np.stack(standardize_mean_list) + standardize_std = np.stack(standardize_std_list) + + self.standardize_param_memo[memo_key] = (standardize_mean, standardize_std) + return standardize_mean, standardize_std + + def standardize(self, array, state, node_type, mean=None, std=None): + if mean is None and std is None: + mean, std = self.get_standardize_params(state, node_type) + elif mean is None and std is not None: + mean, _ = self.get_standardize_params(state, node_type) + elif mean is not None and std is None: + _, std = self.get_standardize_params(state, node_type) + return np.where(np.isnan(array), np.array(np.nan), (array - mean) / std) + + def unstandardize(self, array, state, node_type, mean=None, std=None): + if mean is None and std is None: + mean, std = self.get_standardize_params(state, node_type) + elif mean is None and std is not None: + mean, _ = self.get_standardize_params(state, node_type) + elif mean is not None and std is None: + _, std = self.get_standardize_params(state, node_type) + return array * std + mean + + @property + def scenes_resample_prop(self): + if self._scenes_resample_prop is None: + self._scenes_resample_prop = np.array([scene.resample_prob for scene in self.scenes]) + self._scenes_resample_prop = self._scenes_resample_prop / np.sum(self._scenes_resample_prop) + return self._scenes_resample_prop diff --git a/forge/test/models/pytorch/multimodal/trajectron/trajectron/environment/map.py b/forge/test/models/pytorch/multimodal/trajectron/trajectron/environment/map.py new file mode 100644 index 000000000..47cbb84ca --- /dev/null +++ b/forge/test/models/pytorch/multimodal/trajectron/trajectron/environment/map.py @@ -0,0 +1,201 @@ +# SPDX-FileCopyrightText: (c) 2024 Tenstorrent AI ULC +# +# SPDX-License-Identifier: Apache-2.0 +import torch +import numpy as np +from model.dataset.homography_warper import get_rotation_matrix2d, warp_affine_crop + + +class Map(object): + def __init__(self, data, homography, description=None): + self.data = data + self.homography = homography + self.description = description + + def as_image(self): + raise NotImplementedError + + def get_cropped_maps(self, world_pts, patch_size, rotation=None, device="cpu"): + raise NotImplementedError + + def to_map_points(self, scene_pts): + raise NotImplementedError + + +class GeometricMap(Map): + """ + A Geometric Map is a int tensor of shape [layers, x, y]. The homography must transform a point in scene + coordinates to the respective point in map coordinates. + + :param data: Numpy array of shape [layers, x, y] + :param homography: Numpy array of shape [3, 3] + """ + + def __init__(self, data, homography, description=None): + # assert isinstance(data.dtype, np.floating), "Geometric Maps must be float values." + super(GeometricMap, self).__init__(data, homography, description=description) + + self._last_padding = None + self._last_padded_map = None + self._torch_map = None + + def torch_map(self, device): + if self._torch_map is not None: + return self._torch_map + self._torch_map = torch.tensor(self.data, dtype=torch.uint8, device=device) + return self._torch_map + + def as_image(self): + # We have to transpose x and y to rows and columns. Assumes origin is lower left for image + # Also we move the channels to the last dimension + return (np.transpose(self.data, (2, 1, 0))).astype(np.uint) + + def get_padded_map(self, padding_x, padding_y, device): + if self._last_padding == (padding_x, padding_y): + return self._last_padded_map + else: + self._last_padding = (padding_x, padding_y) + self._last_padded_map = torch.full( + (self.data.shape[0], self.data.shape[1] + 2 * padding_x, self.data.shape[2] + 2 * padding_y), + False, + dtype=torch.uint8, + ) + self._last_padded_map[..., padding_x:-padding_x, padding_y:-padding_y] = self.torch_map(device) + return self._last_padded_map + + @staticmethod + def batch_rotate(map_batched, centers, angles, out_height, out_width): + """ + As the input is a map and the warp_affine works on an image coordinate system we would have to + flip the y axis updown, negate the angles, and flip it back after transformation. + This, however, is the same as not flipping at and not negating the radian. + + :param map_batched: + :param centers: + :param angles: + :param out_height: + :param out_width: + :return: + """ + M = get_rotation_matrix2d(centers, angles, torch.ones_like(angles)) + rotated_map_batched = warp_affine_crop( + map_batched, centers, M, dsize=(out_height, out_width), padding_mode="zeros" + ) + + return rotated_map_batched + + @classmethod + def get_cropped_maps_from_scene_map_batch(cls, maps, scene_pts, patch_size, rotation=None, device="cpu"): + """ + Returns rotated patches of each map around the transformed scene points. + ___________________ + | | | + | |ps[3] | + | | | + | | | + | o|__________| + | | ps[2] | + | | | + |_______|__________| + ps = patch_size + + :param maps: List of GeometricMap objects [bs] + :param scene_pts: Scene points: [bs, 2] + :param patch_size: Extracted Patch size after rotation: [-x, -y, +x, +y] + :param rotation: Rotations in degrees: [bs] + :param device: Device on which the rotated tensors should be returned. + :return: Rotated and cropped tensor patches. + """ + batch_size = scene_pts.shape[0] + lat_size = 2 * np.max((patch_size[0], patch_size[2])) + long_size = 2 * np.max((patch_size[1], patch_size[3])) + assert lat_size % 2 == 0, "Patch width must be divisible by 2" + assert long_size % 2 == 0, "Patch length must be divisible by 2" + lat_size_half = lat_size // 2 + long_size_half = long_size // 2 + + context_padding_x = int(np.ceil(np.sqrt(2) * lat_size)) + context_padding_y = int(np.ceil(np.sqrt(2) * long_size)) + + centers = torch.tensor( + [s_map.to_map_points(scene_pts[np.newaxis, i]) for i, s_map in enumerate(maps)], + dtype=torch.long, + device=device, + ).squeeze(dim=1) + torch.tensor([context_padding_x, context_padding_y], device=device, dtype=torch.long) + + padded_map = [s_map.get_padded_map(context_padding_x, context_padding_y, device=device) for s_map in maps] + + padded_map_batched = torch.stack( + [ + padded_map[i][ + ..., + centers[i, 0] - context_padding_x : centers[i, 0] + context_padding_x, + centers[i, 1] - context_padding_y : centers[i, 1] + context_padding_y, + ] + for i in range(centers.shape[0]) + ], + dim=0, + ) + + center_patches = torch.tensor([[context_padding_y, context_padding_x]], dtype=torch.int, device=device).repeat( + batch_size, 1 + ) + + if rotation is not None: + angles = torch.Tensor(rotation) + else: + angles = torch.zeros(batch_size) + + rotated_map_batched = cls.batch_rotate( + padded_map_batched / 255.0, center_patches.float(), angles, long_size, lat_size + ) + + del padded_map_batched + + return rotated_map_batched[ + ..., + long_size_half - patch_size[1] : (long_size_half + patch_size[3]), + lat_size_half - patch_size[0] : (lat_size_half + patch_size[2]), + ] + + def get_cropped_maps(self, scene_pts, patch_size, rotation=None, device="cpu"): + """ + Returns rotated patches of the map around the transformed scene points. + ___________________ + | | | + | |ps[3] | + | | | + | | | + | o|__________| + | | ps[2] | + | | | + |_______|__________| + ps = patch_size + + :param scene_pts: Scene points: [bs, 2] + :param patch_size: Extracted Patch size after rotation: [-lat, -long, +lat, +long] + :param rotation: Rotations in degrees: [bs] + :param device: Device on which the rotated tensors should be returned. + :return: Rotated and cropped tensor patches. + """ + return self.get_cropped_maps_from_scene_map_batch( + [self] * scene_pts.shape[0], scene_pts, patch_size, rotation=rotation, device=device + ) + + def to_map_points(self, scene_pts): + org_shape = None + if len(scene_pts.shape) > 2: + org_shape = scene_pts.shape + scene_pts = scene_pts.reshape((-1, 2)) + N, dims = scene_pts.shape + points_with_one = np.ones((dims + 1, N)) + points_with_one[:dims] = scene_pts.T + map_points = (self.homography @ points_with_one).T[..., :dims] + if org_shape is not None: + map_points = map_points.reshape(org_shape) + return map_points + + +class ImageMap(Map): # TODO Implement for image maps -> watch flipped coordinate system + def __init__(self): + raise NotImplementedError diff --git a/forge/test/models/pytorch/multimodal/trajectron/trajectron/environment/node.py b/forge/test/models/pytorch/multimodal/trajectron/trajectron/environment/node.py new file mode 100644 index 000000000..b27b3ce17 --- /dev/null +++ b/forge/test/models/pytorch/multimodal/trajectron/trajectron/environment/node.py @@ -0,0 +1,256 @@ +# SPDX-FileCopyrightText: (c) 2024 Tenstorrent AI ULC +# +# SPDX-License-Identifier: Apache-2.0 +import random +import numpy as np +import pandas as pd +from .data_structures import DoubleHeaderNumpyArray + +# from ncls import NCLS + + +class Node(object): + def __init__( + self, + node_type, + node_id, + data, + length=None, + width=None, + height=None, + first_timestep=0, + is_robot=False, + description="", + frequency_multiplier=1, + non_aug_node=None, + ): + self.type = node_type + self.id = node_id + self.length = length + self.width = width + self.height = height + self.first_timestep = first_timestep + self.non_aug_node = non_aug_node + + if data is not None: + if isinstance(data, pd.DataFrame): + self.data = DoubleHeaderNumpyArray(data.values, list(data.columns)) + elif isinstance(data, DoubleHeaderNumpyArray): + self.data = data + else: + self.data = None + + self.is_robot = is_robot + self._last_timestep = None + self.description = description + self.frequency_multiplier = frequency_multiplier + + self.forward_in_time_on_next_override = False + + def __eq__(self, other): + return ( + (isinstance(other, self.__class__) or isinstance(self, other.__class__)) + and self.id == other.id + and self.type == other.type + ) + + def __ne__(self, other): + return not self.__eq__(other) + + def __hash__(self): + return hash((self.type, self.id)) + + def __repr__(self): + return "/".join([self.type.name, self.id]) + + def overwrite_data(self, data, header, forward_in_time_on_next_overwrite=False): + """ + This function hard overwrites the data matrix. When using it you have to make sure that the columns + in the new data matrix correspond to the old structure. As well as setting first_timestep. + + :param data: New data matrix + :param forward_in_time_on_next_overwrite: On the !!NEXT!! call of overwrite_data first_timestep will be increased. + :return: None + """ + if header is None: + self.data.data = data + else: + self.data = DoubleHeaderNumpyArray(data, header) + + self._last_timestep = None + if self.forward_in_time_on_next_override: + self.first_timestep += 1 + self.forward_in_time_on_next_override = forward_in_time_on_next_overwrite + + def scene_ts_to_node_ts(self, scene_ts) -> (np.ndarray, int, int): + """ + Transforms timestamp from scene into timeframe of node data. + + :param scene_ts: Scene timesteps + :return: ts: Transformed timesteps, paddingl: Number of timesteps in scene range which are not available in + node data before data is available. paddingu: Number of timesteps in scene range which are not + available in node data after data is available. + """ + paddingl = (self.first_timestep - scene_ts[0]).clip(0) + paddingu = (scene_ts[1] - self.last_timestep).clip(0) + ts = np.array(scene_ts).clip(min=self.first_timestep, max=self.last_timestep) - self.first_timestep + return ts, paddingl, paddingu + + def history_points_at(self, ts) -> int: + """ + Number of history points in trajectory. Timestep is exclusive. + + :param ts: Scene timestep where the number of history points are queried. + :return: Number of history timesteps. + """ + return ts - self.first_timestep + + def get(self, tr_scene, state, padding=np.nan) -> np.ndarray: + """ + Returns a time range of multiple properties of the node. + + :param tr_scene: The timestep range (inklusive). + :param state: The state description for which the properties are returned. + :param padding: The value which should be used for padding if not enough information is available. + :return: Array of node property values. + """ + if tr_scene.size == 1: + tr_scene = np.array([tr_scene[0], tr_scene[0]]) + length = tr_scene[1] - tr_scene[0] + 1 # tr is inclusive + tr, paddingl, paddingu = self.scene_ts_to_node_ts(tr_scene) + data_array = self.data[tr[0] : tr[1] + 1, state] + padded_data_array = np.full((length, data_array.shape[1]), fill_value=padding) + padded_data_array[paddingl : length - paddingu] = data_array + return padded_data_array + + @property + def timesteps(self) -> int: + """ + Number of available timesteps for node. + + :return: Number of available timesteps. + """ + return self.data.shape[0] + + @property + def last_timestep(self) -> int: + """ + Nodes last timestep in the Scene. + + :return: Nodes last timestep. + """ + if self._last_timestep is None: + self._last_timestep = self.first_timestep + self.timesteps - 1 + return self._last_timestep + + +class MultiNode(Node): + def __init__(self, node_type, node_id, nodes_list, is_robot=False): + super(MultiNode, self).__init__(node_type, node_id, data=None, is_robot=is_robot) + self.nodes_list = nodes_list + for node in self.nodes_list: + node.is_robot = is_robot + + self.first_timestep = min(node.first_timestep for node in self.nodes_list) + self._last_timestep = max(node.last_timestep for node in self.nodes_list) + + starts = np.array([node.first_timestep for node in self.nodes_list], dtype=np.int64) + ends = np.array([node.last_timestep for node in self.nodes_list], dtype=np.int64) + ids = np.arange(len(self.nodes_list), dtype=np.int64) + self.interval_tree = NCLS(starts, ends, ids) + + @staticmethod + def find_non_overlapping_nodes(nodes_list, min_timesteps=1) -> list: + """ + Greedily finds a set of non-overlapping nodes in the provided scene. + + :return: A list of non-overlapping nodes. + """ + non_overlapping_nodes = list() + nodes = sorted(nodes_list, key=lambda n: n.last_timestep) + current_time = 0 + for node in nodes: + if node.first_timestep >= current_time and node.timesteps >= min_timesteps: + # Include the node + non_overlapping_nodes.append(node) + current_time = node.last_timestep + + return non_overlapping_nodes + + def get_node_at_timesteps(self, scene_ts) -> Node: + possible_node_ranges = list(self.interval_tree.find_overlap(scene_ts[0], scene_ts[1] + 1)) + if not possible_node_ranges: + return Node( + node_type=self.type, node_id="EMPTY", data=self.nodes_list[0].data * np.nan, is_robot=self.is_robot + ) + + node_idx = random.choice(possible_node_ranges)[2] + return self.nodes_list[node_idx] + + def scene_ts_to_node_ts(self, scene_ts) -> (Node, np.ndarray, int, int): + """ + Transforms timestamp from scene into timeframe of node data. + + :param scene_ts: Scene timesteps + :return: ts: Transformed timesteps, paddingl: Number of timesteps in scene range which are not available in + node data before data is available. paddingu: Number of timesteps in scene range which are not + available in node data after data is available. + """ + possible_node_ranges = list(self.interval_tree.find_overlap(scene_ts[0], scene_ts[1] + 1)) + if not possible_node_ranges: + return None, None, None, None + + node_idx = random.choice(possible_node_ranges)[2] + node = self.nodes_list[node_idx] + + paddingl = (node.first_timestep - scene_ts[0]).clip(0) + paddingu = (scene_ts[1] - node.last_timestep).clip(0) + ts = np.array(scene_ts).clip(min=node.first_timestep, max=node.last_timestep) - node.first_timestep + return node, ts, paddingl, paddingu + + def get(self, tr_scene, state, padding=np.nan) -> np.ndarray: + if tr_scene.size == 1: + tr_scene = np.array([tr_scene, tr_scene]) + length = tr_scene[1] - tr_scene[0] + 1 # tr is inclusive + + node, tr, paddingl, paddingu = self.scene_ts_to_node_ts(tr_scene) + if node is None: + state_length = sum([len(entity_dims) for entity_dims in state.values()]) + return np.full((length, state_length), fill_value=padding) + + data_array = node.data[tr[0] : tr[1] + 1, state] + padded_data_array = np.full((length, data_array.shape[1]), fill_value=padding) + padded_data_array[paddingl : length - paddingu] = data_array + return padded_data_array + + def get_all(self, tr_scene, state, padding=np.nan) -> np.ndarray: + # Assumption here is that the user is asking for all of the data in this MultiNode and to return it within a + # full scene-sized output array. + assert tr_scene.size == 2 and tr_scene[0] == 0 and self.last_timestep <= tr_scene[1] + length = tr_scene[1] - tr_scene[0] + 1 # tr is inclusive + state_length = sum([len(entity_dims) for entity_dims in state.values()]) + padded_data_array = np.full((length, state_length), fill_value=padding) + for node in self.nodes_list: + padded_data_array[node.first_timestep : node.last_timestep + 1] = node.data[:, state] + + return padded_data_array + + def history_points_at(self, ts) -> int: + """ + Number of history points in trajectory. Timestep is exclusive. + + :param ts: Scene timestep where the number of history points are queried. + :return: Number of history timesteps. + """ + node_idx = next(self.interval_tree.find_overlap(ts, ts + 1))[2] + node = self.nodes_list[node_idx] + return ts - node.first_timestep + + @property + def timesteps(self) -> int: + """ + Number of available timesteps for node. + + :return: Number of available timesteps. + """ + return self._last_timestep - self.first_timestep + 1 diff --git a/forge/test/models/pytorch/multimodal/trajectron/trajectron/environment/node_type.py b/forge/test/models/pytorch/multimodal/trajectron/trajectron/environment/node_type.py new file mode 100644 index 000000000..1513e487a --- /dev/null +++ b/forge/test/models/pytorch/multimodal/trajectron/trajectron/environment/node_type.py @@ -0,0 +1,38 @@ +# SPDX-FileCopyrightText: (c) 2024 Tenstorrent AI ULC +# +# SPDX-License-Identifier: Apache-2.0 +class NodeType(object): + def __init__(self, name, value): + self.name = name + self.value = value + + def __repr__(self): + return self.name + + def __eq__(self, other): + if type(other) == str and self.name == other: + return True + else: + return isinstance(other, self.__class__) and self.name == other.name + + def __ne__(self, other): + return not self.__eq__(other) + + def __hash__(self): + return hash(self.name) + + def __add__(self, other): + return self.name + other + + +class NodeTypeEnum(list): + def __init__(self, node_type_list): + self.node_type_list = node_type_list + node_types = [NodeType(name, node_type_list.index(name) + 1) for name in node_type_list] + super().__init__(node_types) + + def __getattr__(self, name): + if not name.startswith("_") and name in object.__getattribute__(self, "node_type_list"): + return self[object.__getattribute__(self, "node_type_list").index(name)] + else: + return object.__getattribute__(self, name) diff --git a/forge/test/models/pytorch/multimodal/trajectron/trajectron/environment/scene.py b/forge/test/models/pytorch/multimodal/trajectron/trajectron/environment/scene.py new file mode 100644 index 000000000..38430e607 --- /dev/null +++ b/forge/test/models/pytorch/multimodal/trajectron/trajectron/environment/scene.py @@ -0,0 +1,218 @@ +# SPDX-FileCopyrightText: (c) 2024 Tenstorrent AI ULC +# +# SPDX-License-Identifier: Apache-2.0 +import copy +import numpy as np +from .scene_graph import TemporalSceneGraph, SceneGraph +from .node import MultiNode + + +class Scene(object): + def __init__(self, timesteps, map=None, dt=1, name="", frequency_multiplier=1, aug_func=None, non_aug_scene=None): + self.map = map + self.timesteps = timesteps + self.dt = dt + self.name = name + + self.nodes = [] + + self.robot = None + + self.temporal_scene_graph = None + + self.frequency_multiplier = frequency_multiplier + + self.description = "" + + self.aug_func = aug_func + self.non_aug_scene = non_aug_scene + + def add_robot_from_nodes(self, robot_type): + scenes = [self] + if hasattr(self, "augmented"): + scenes += self.augmented + + for scn in scenes: + nodes_list = [node for node in scn.nodes if node.type == robot_type] + non_overlapping_nodes = MultiNode.find_non_overlapping_nodes(nodes_list, min_timesteps=3) + scn.robot = MultiNode(robot_type, "ROBOT", non_overlapping_nodes, is_robot=True) + + for node in non_overlapping_nodes: + scn.nodes.remove(node) + scn.nodes.append(scn.robot) + + def get_clipped_input_dict(self, timestep, state): + input_dict = dict() + existing_nodes = self.get_nodes_clipped_at_time(timesteps=np.array([timestep]), state=state) + tr_scene = np.array([timestep, timestep]) + for node in existing_nodes: + input_dict[node] = node.get(tr_scene, state[node.type]) + + return input_dict + + def get_scene_graph( + self, timestep, attention_radius=None, edge_addition_filter=None, edge_removal_filter=None + ) -> SceneGraph: + """ + Returns the Scene Graph for a given timestep. If the Temporal Scene Graph was pre calculated, + the temporal scene graph is sliced. Otherwise the scene graph is calculated on the spot. + + :param timestep: Timestep for which the scene graph is returned. + :param attention_radius: Attention radius for each node type permutation. (Only online) + :param edge_addition_filter: Filter for adding edges (Only online) + :param edge_removal_filter: Filter for removing edges (Only online) + :return: Scene Graph for given timestep. + """ + if self.temporal_scene_graph is None: + timestep_range = np.array([timestep - len(edge_removal_filter), timestep]) + node_pos_dict = dict() + present_nodes = self.present_nodes(np.array([timestep])) + + for node in present_nodes[timestep]: + node_pos_dict[node] = np.squeeze(node.get(timestep_range, {"position": ["x", "y"]})) + tsg = TemporalSceneGraph.create_from_temp_scene_dict( + node_pos_dict, + attention_radius, + duration=(len(edge_removal_filter) + 1), + edge_addition_filter=edge_addition_filter, + edge_removal_filter=edge_removal_filter, + ) + + return tsg.to_scene_graph( + t=len(edge_removal_filter), t_hist=len(edge_removal_filter), t_fut=len(edge_addition_filter) + ) + else: + return self.temporal_scene_graph.to_scene_graph( + timestep, len(edge_removal_filter), len(edge_addition_filter) + ) + + def calculate_scene_graph(self, attention_radius, edge_addition_filter=None, edge_removal_filter=None) -> None: + """ + Calculate the Temporal Scene Graph for the entire Scene. + + :param attention_radius: Attention radius for each node type permutation. + :param edge_addition_filter: Filter for adding edges. + :param edge_removal_filter: Filter for removing edges. + :return: None + """ + timestep_range = np.array([0, self.timesteps - 1]) + node_pos_dict = dict() + + for node in self.nodes: + if type(node) is MultiNode: + node_pos_dict[node] = np.squeeze(node.get_all(timestep_range, {"position": ["x", "y"]})) + else: + node_pos_dict[node] = np.squeeze(node.get(timestep_range, {"position": ["x", "y"]})) + + self.temporal_scene_graph = TemporalSceneGraph.create_from_temp_scene_dict( + node_pos_dict, + attention_radius, + duration=self.timesteps, + edge_addition_filter=edge_addition_filter, + edge_removal_filter=edge_removal_filter, + ) + + def duration(self): + """ + Calculates the duration of the scene. + + :return: Duration of the scene in s. + """ + return self.timesteps * self.dt + + def present_nodes( + self, timesteps, type=None, min_history_timesteps=0, min_future_timesteps=0, return_robot=True + ) -> dict: + """ + Finds all present nodes in the scene at a given timestemp + + :param timesteps: Timestep(s) for which all present nodes should be returned + :param type: Node type which should be returned. If None all node types are returned. + :param min_history_timesteps: Minimum history timesteps of a node to be returned. + :param min_future_timesteps: Minimum future timesteps of a node to be returned. + :param return_robot: Return a node if it is the robot. + :return: Dictionary with timesteps as keys and list of nodes as value. + """ + + present_nodes = {} + + for node in self.nodes: + if node.is_robot and not return_robot: + continue + if type is None or node.type == type: + lower_bound = timesteps - min_history_timesteps + upper_bound = timesteps + min_future_timesteps + mask = (node.first_timestep <= lower_bound) & (upper_bound <= node.last_timestep) + if mask.any(): + timestep_indices_present = np.nonzero(mask)[0] + for timestep_index_present in timestep_indices_present: + if timesteps[timestep_index_present] in present_nodes.keys(): + present_nodes[timesteps[timestep_index_present]].append(node) + else: + present_nodes[timesteps[timestep_index_present]] = [node] + + return present_nodes + + def get_nodes_clipped_at_time(self, timesteps, state): + clipped_nodes = list() + + existing_nodes = self.present_nodes(timesteps) + all_nodes = set().union(*existing_nodes.values()) + if not all_nodes: + return clipped_nodes + + tr_scene = np.array([timesteps.min(), timesteps.max()]) + data_header_memo = dict() + for node in all_nodes: + if isinstance(node, MultiNode): + copied_node = copy.deepcopy(node.get_node_at_timesteps(tr_scene)) + copied_node.id = self.robot.id + else: + copied_node = copy.deepcopy(node) + + clipped_value = node.get(tr_scene, state[node.type]) + + if node.type not in data_header_memo: + data_header = list() + for quantity, values in state[node.type].items(): + for value in values: + data_header.append((quantity, value)) + + data_header_memo[node.type] = data_header + + copied_node.overwrite_data(clipped_value, data_header_memo[node.type]) + copied_node.first_timestep = tr_scene[0] + + clipped_nodes.append(copied_node) + + return clipped_nodes + + def sample_timesteps(self, batch_size, min_future_timesteps=0) -> np.ndarray: + """ + Sample a batch size of possible timesteps for the scene. + + :param batch_size: Number of timesteps to sample. + :param min_future_timesteps: Minimum future timesteps in the scene for a timestep to be returned. + :return: Numpy Array of sampled timesteps. + """ + if batch_size > self.timesteps: + batch_size = self.timesteps + return np.random.choice(np.arange(0, self.timesteps - min_future_timesteps), size=batch_size, replace=False) + + def augment(self): + if self.aug_func is not None: + return self.aug_func(self) + else: + return self + + def get_node_by_id(self, id): + for node in self.nodes: + if node.id == id: + return node + + def __repr__(self): + return ( + f"Scene: Duration: {self.duration()}s," + f" Nodes: {len(self.nodes)}," + f" Map: {'Yes' if self.map is not None else 'No'}." + ) diff --git a/forge/test/models/pytorch/multimodal/trajectron/trajectron/environment/scene_graph.py b/forge/test/models/pytorch/multimodal/trajectron/trajectron/environment/scene_graph.py new file mode 100644 index 000000000..63c15bb2c --- /dev/null +++ b/forge/test/models/pytorch/multimodal/trajectron/trajectron/environment/scene_graph.py @@ -0,0 +1,536 @@ +# SPDX-FileCopyrightText: (c) 2024 Tenstorrent AI ULC +# +# SPDX-License-Identifier: Apache-2.0 +import numpy as np +from scipy.spatial.distance import pdist, squareform +import scipy.signal as ss +from collections import defaultdict +import warnings +from .node import Node + + +class Edge(object): + def __init__(self, curr_node, other_node): + self.id = self.get_edge_id(curr_node, other_node) + self.type = self.get_edge_type(curr_node, other_node) + self.curr_node = curr_node + self.other_node = other_node + + @staticmethod + def get_edge_id(n1, n2): + raise NotImplementedError("Use one of the Edge subclasses!") + + @staticmethod + def get_str_from_types(nt1, nt2): + raise NotImplementedError("Use one of the Edge subclasses!") + + @staticmethod + def get_edge_type(n1, n2): + raise NotImplementedError("Use one of the Edge subclasses!") + + def __eq__(self, other): + return isinstance(other, self.__class__) and self.id == other.id + + def __ne__(self, other): + return not self.__eq__(other) + + def __hash__(self): + return hash(self.id) + + def __repr__(self): + return self.id + + +class UndirectedEdge(Edge): + def __init__(self, curr_node, other_node): + super(UndirectedEdge, self).__init__(curr_node, other_node) + + @staticmethod + def get_edge_id(n1, n2): + return "-".join(sorted([str(n1), str(n2)])) + + @staticmethod + def get_str_from_types(nt1, nt2): + return "-".join(sorted([nt1.name, nt2.name])) + + @staticmethod + def get_edge_type(n1, n2): + return "-".join(sorted([n1.type.name, n2.type.name])) + + +class DirectedEdge(Edge): + def __init__(self, curr_node, other_node): + super(DirectedEdge, self).__init__(curr_node, other_node) + + @staticmethod + def get_edge_id(n1, n2): + return "->".join([str(n1), str(n2)]) + + @staticmethod + def get_str_from_types(nt1, nt2): + return "->".join([nt1.name, nt2.name]) + + @staticmethod + def get_edge_type(n1, n2): + return "->".join([n1.type.name, n2.type.name]) + + +class TemporalSceneGraph(object): + def __init__( + self, + edge_radius, + nodes=None, + adj_cube=np.zeros((1, 0, 0)), + weight_cube=np.zeros((1, 0, 0)), + node_type_mat=np.zeros((0, 0)), + edge_scaling=None, + ): + self.edge_radius = edge_radius + self.nodes = nodes + if nodes is None: + self.nodes = np.array([]) + self.adj_cube = adj_cube + self.weight_cube = weight_cube + self.node_type_mat = node_type_mat + self.adj_mat = np.max(self.adj_cube, axis=0).clip(max=1.0) + self.edge_scaling = edge_scaling + self.node_index_lookup = None + self.calculate_node_index_lookup() + + def calculate_node_index_lookup(self): + node_index_lookup = dict() + for i, node in enumerate(self.nodes): + node_index_lookup[node] = i + + self.node_index_lookup = node_index_lookup + + def get_num_edges(self, t=0): + return np.sum(self.adj_cube[t]) // 2 + + def get_index(self, node): + return self.node_index_lookup[node] + + @classmethod + def create_from_temp_scene_dict( + cls, + scene_temp_dict, + attention_radius, + duration=1, + edge_addition_filter=None, + edge_removal_filter=None, + online=False, + ): + """ + Construct a spatiotemporal graph from node positions in a dataset. + + :param scene_temp_dict: Dict with all nodes in scene as keys and np.ndarray with positions as value + :param attention_radius: Attention radius dict. + :param duration: Temporal duration of the graph. + :param edge_addition_filter: - + :param edge_removal_filter: - + :return: TemporalSceneGraph + """ + + nodes = scene_temp_dict.keys() + N = len(nodes) + total_timesteps = duration + + if N == 0: + return TemporalSceneGraph(attention_radius) + + position_cube = np.full((total_timesteps, N, 2), np.nan) + + adj_cube = np.zeros((total_timesteps, N, N), dtype=np.int8) + dist_cube = np.zeros((total_timesteps, N, N), dtype=np.float) + + node_type_mat = np.zeros((N, N), dtype=np.int8) + node_attention_mat = np.zeros((N, N), dtype=np.float) + + for node_idx, node in enumerate(nodes): + if online: + # RingBuffers do not have a fixed constant size. Instead, they grow up to their capacity. Thus, + # we need to fill the values preceding the RingBuffer values with NaNs to make them fill the + # position_cube. + position_cube[-scene_temp_dict[node].shape[0] :, node_idx] = scene_temp_dict[node] + else: + position_cube[:, node_idx] = scene_temp_dict[node] + + node_type_mat[:, node_idx] = node.type.value + for node_idx_from, node_from in enumerate(nodes): + node_attention_mat[node_idx_from, node_idx] = attention_radius[(node_from.type, node.type)] + + np.fill_diagonal(node_type_mat, 0) + + for timestep in range(position_cube.shape[0]): + dists = squareform(pdist(position_cube[timestep], metric="euclidean")) + + # Put a 1 for all agent pairs which are closer than the edge_radius. + # Can produce a warning as dists can be nan if no data for node is available. + # This is accepted as nan <= x evaluates to False + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + adj_matrix = (dists <= node_attention_mat).astype(np.int8) * node_type_mat + + # Remove self-loops. + np.fill_diagonal(adj_matrix, 0) + + adj_cube[timestep] = adj_matrix + dist_cube[timestep] = dists + + dist_cube[np.isnan(dist_cube)] = 0.0 + weight_cube = np.divide(1.0, dist_cube, out=np.zeros_like(dist_cube), where=(dist_cube > 0.0)) + edge_scaling = None + if edge_addition_filter is not None and edge_removal_filter is not None: + edge_scaling = cls.calculate_edge_scaling(adj_cube, edge_addition_filter, edge_removal_filter) + tsg = cls( + attention_radius, np.array(list(nodes)), adj_cube, weight_cube, node_type_mat, edge_scaling=edge_scaling + ) + return tsg + + @staticmethod + def calculate_edge_scaling(adj_cube, edge_addition_filter, edge_removal_filter): + shifted_right = np.pad( + adj_cube, ((len(edge_addition_filter) - 1, 0), (0, 0), (0, 0)), "constant", constant_values=0 + ) + + new_edges = np.minimum(ss.convolve(shifted_right, np.reshape(edge_addition_filter, (-1, 1, 1)), "full"), 1.0)[ + (len(edge_addition_filter) - 1) : -(len(edge_addition_filter) - 1) + ] + + new_edges[adj_cube == 0] = 0 + + result = np.minimum(ss.convolve(new_edges, np.reshape(edge_removal_filter, (-1, 1, 1)), "full"), 1.0)[ + : -(len(edge_removal_filter) - 1) + ] + + return result + + def to_scene_graph(self, t, t_hist=0, t_fut=0): + """ + Creates a Scene Graph from a Temporal Scene Graph + + :param t: Time in Temporal Scene Graph for which Scene Graph is created. + :param t_hist: Number of history timesteps which are considered to form edges in Scene Graph. + :param t_fut: Number of future timesteps which are considered to form edges in Scene Graph. + :return: SceneGraph + """ + lower_t = np.clip(t - t_hist, a_min=0, a_max=None) + higher_t = np.clip(t + t_fut + 1, a_min=None, a_max=self.adj_cube.shape[0] + 1) + adj_mat = np.max(self.adj_cube[lower_t:higher_t], axis=0) + weight_mat = np.max(self.weight_cube[lower_t:higher_t], axis=0) + return SceneGraph( + self.edge_radius, + self.nodes, + adj_mat, + weight_mat, + self.node_type_mat, + self.node_index_lookup, + edge_scaling=self.edge_scaling[t] if self.edge_scaling is not None else None, + ) + + +class SceneGraph(object): + def __init__( + self, + edge_radius, + nodes=None, + adj_mat=np.zeros((0, 0)), + weight_mat=np.zeros((0, 0)), + node_type_mat=np.zeros((0, 0)), + node_index_lookup=None, + edge_scaling=None, + ): + self.edge_radius = edge_radius + self.nodes = nodes + if nodes is None: + self.nodes = np.array([]) + self.node_type_mat = node_type_mat + self.adj_mat = adj_mat + self.weight_mat = weight_mat + self.edge_scaling = edge_scaling + self.node_index_lookup = node_index_lookup + + def get_index(self, node): + return self.node_index_lookup[node] + + def get_num_edges(self): + return np.sum(self.adj_mat) // 2 + + def get_neighbors(self, node, node_type): + """ + Get all neighbors of a node. + + :param node: Node for which all neighbors are returned. + :param node_type: Specifies node types which are returned. + :return: List of all neighbors. + """ + node_index = self.get_index(node) + connection_mask = self.get_connection_mask(node_index) + mask = (self.node_type_mat[node_index] == node_type.value) * connection_mask + return self.nodes[mask] + + def get_edge_scaling(self, node=None): + if node is None: + return self.edge_scaling + else: + node_index = self.get_index(node) + connection_mask = self.get_connection_mask(node_index) + return self.edge_scaling[node_index, connection_mask] + + def get_edge_weight(self, node=None): + if node is None: + return self.weight_mat + else: + node_index = self.get_index(node) + connection_mask = self.get_connection_mask(node_index) + return self.weight_mat[node_index, connection_mask] + + def get_connection_mask(self, node_index): + if self.edge_scaling is None: # We do not use edge scaling + return self.adj_mat[node_index] > 0.0 + else: + return self.edge_scaling[node_index] > 1e-2 + + def __sub__(self, other): + new_nodes = [node for node in self.nodes if node not in other.nodes] + removed_nodes = [node for node in other.nodes if node not in self.nodes] + + our_types = set(node.type for node in self.nodes) + other_types = set(node.type for node in other.nodes) + all_node_types = our_types | other_types + + new_neighbors = defaultdict(lambda: defaultdict(set)) + for node in self.nodes: + if node in removed_nodes: + continue + + if node in other.nodes: + for node_type in all_node_types: + new_items = set(self.get_neighbors(node, node_type)) - set(other.get_neighbors(node, node_type)) + if len(new_items) > 0: + new_neighbors[node][DirectedEdge.get_edge_type(node, Node(node_type, None, None))] = new_items + else: + for node_type in our_types: + neighbors = self.get_neighbors(node, node_type) + if len(neighbors) > 0: + new_neighbors[node][DirectedEdge.get_edge_type(node, Node(node_type, None, None))] = set( + neighbors + ) + + removed_neighbors = defaultdict(lambda: defaultdict(set)) + for node in other.nodes: + if node in removed_nodes: + continue + + if node in self.nodes: + for node_type in all_node_types: + removed_items = set(other.get_neighbors(node, node_type)) - set(self.get_neighbors(node, node_type)) + if len(removed_items) > 0: + removed_neighbors[node][ + DirectedEdge.get_edge_type(node, Node(node_type, None, None)) + ] = removed_items + else: + for node_type in other_types: + neighbors = other.get_neighbors(node, node_type) + if len(neighbors) > 0: + removed_neighbors[node][DirectedEdge.get_edge_type(node, Node(node_type, None, None))] = set( + neighbors + ) + + return new_nodes, removed_nodes, new_neighbors, removed_neighbors + + +if __name__ == "__main__": + from environment import NodeTypeEnum + import time + + # # # # # # # # # # # # # # # # # + # Testing edge mask calculation # + # # # # # # # # # # # # # # # # # + B = np.array( + [ + [0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0], + [1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0], + [1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0], + [1, 1, 1, 1, 1, 0, 1, 0, 0, 1, 1, 1, 0], + ] + )[:, :, np.newaxis, np.newaxis] + print(B.shape) + + edge_addition_filter = [0.25, 0.5, 0.75, 1.0] + edge_removal_filter = [1.0, 0.5, 0.0] + for i in range(B.shape[0]): + A = B[i] # (time, N, N) + + print(A[:, 0, 0]) + + start = time.time() + new_edges = np.minimum(ss.convolve(A, np.reshape(edge_addition_filter, (-1, 1, 1)), "full"), 1.0)[ + (len(edge_addition_filter) - 1) : + ] + old_edges = np.minimum(ss.convolve(A, np.reshape(edge_removal_filter, (-1, 1, 1)), "full"), 1.0)[ + : -(len(edge_removal_filter) - 1) + ] + res = np.minimum(new_edges + old_edges, 1.0)[:, 0, 0] + end = time.time() + print(end - start) + print(res) + + start = time.time() + res = TemporalSceneGraph.calculate_edge_scaling(A, edge_addition_filter, edge_removal_filter)[:, 0, 0] + end = time.time() + print(end - start) + print(res) + + print("-" * 40) + + # # # # # # # # # # # # # # # + # Testing graph subtraction # + # # # # # # # # # # # # # # # + print("\n" + "-" * 40 + "\n") + + node_type_list = ["PEDESTRIAN", "BICYCLE", "VEHICLE"] + nte = NodeTypeEnum(node_type_list) + + attention_radius = dict() + attention_radius[(nte.PEDESTRIAN, nte.PEDESTRIAN)] = 5.0 + attention_radius[(nte.PEDESTRIAN, nte.VEHICLE)] = 20.0 + attention_radius[(nte.PEDESTRIAN, nte.BICYCLE)] = 10.0 + attention_radius[(nte.VEHICLE, nte.PEDESTRIAN)] = 20.0 + attention_radius[(nte.VEHICLE, nte.VEHICLE)] = 20.0 + attention_radius[(nte.VEHICLE, nte.BICYCLE)] = 20.0 + attention_radius[(nte.BICYCLE, nte.PEDESTRIAN)] = 10.0 + attention_radius[(nte.BICYCLE, nte.VEHICLE)] = 20.0 + attention_radius[(nte.BICYCLE, nte.BICYCLE)] = 10.0 + + scene_dict1 = { + Node(nte.PEDESTRIAN, node_id="1"): np.array([1, 0]), + Node(nte.PEDESTRIAN, node_id="2"): np.array([0, 1]), + } + sg1 = TemporalSceneGraph.create_from_temp_scene_dict( + scene_dict1, + attention_radius=attention_radius, + duration=1, + edge_addition_filter=[0.25, 0.5, 0.75, 1.0], + edge_removal_filter=[1.0, 0.0], + ).to_scene_graph(t=0) + + scene_dict2 = { + Node(nte.PEDESTRIAN, node_id="1"): np.array([1, 0]), + Node(nte.PEDESTRIAN, node_id="2"): np.array([1, 1]), + } + sg2 = TemporalSceneGraph.create_from_temp_scene_dict( + scene_dict2, + attention_radius=attention_radius, + duration=1, + edge_addition_filter=[0.25, 0.5, 0.75, 1.0], + edge_removal_filter=[1.0, 0.0], + ).to_scene_graph(t=0) + + new_nodes, removed_nodes, new_neighbors, removed_neighbors = sg2 - sg1 + print("New Nodes:", new_nodes) + print("Removed Nodes:", removed_nodes) + print("New Neighbors:", new_neighbors) + print("Removed Neighbors:", removed_neighbors) + + # # # # # # # # # # # # # # # + print("\n" + "-" * 40 + "\n") + + scene_dict1 = { + Node(nte.PEDESTRIAN, node_id="1"): np.array([1, 0]), + Node(nte.PEDESTRIAN, node_id="2"): np.array([0, 1]), + } + sg1 = TemporalSceneGraph.create_from_temp_scene_dict( + scene_dict1, + attention_radius=attention_radius, + duration=1, + edge_addition_filter=[0.25, 0.5, 0.75, 1.0], + edge_removal_filter=[1.0, 0.0], + ).to_scene_graph(t=0) + + scene_dict2 = { + Node(nte.PEDESTRIAN, node_id="1"): np.array([1, 0]), + Node(nte.PEDESTRIAN, node_id="2"): np.array([1, 1]), + Node(nte.PEDESTRIAN, node_id="3"): np.array([20, 1]), + } + sg2 = TemporalSceneGraph.create_from_temp_scene_dict( + scene_dict2, + attention_radius=attention_radius, + duration=1, + edge_addition_filter=[0.25, 0.5, 0.75, 1.0], + edge_removal_filter=[1.0, 0.0], + ).to_scene_graph(t=0) + + new_nodes, removed_nodes, new_neighbors, removed_neighbors = sg2 - sg1 + print("New Nodes:", new_nodes) + print("Removed Nodes:", removed_nodes) + print("New Neighbors:", new_neighbors) + print("Removed Neighbors:", removed_neighbors) + + # # # # # # # # # # # # # # # + print("\n" + "-" * 40 + "\n") + + scene_dict1 = { + Node(nte.PEDESTRIAN, node_id="1"): np.array([1, 0]), + Node(nte.PEDESTRIAN, node_id="2"): np.array([0, 1]), + } + sg1 = TemporalSceneGraph.create_from_temp_scene_dict( + scene_dict1, + attention_radius=attention_radius, + duration=1, + edge_addition_filter=[0.25, 0.5, 0.75, 1.0], + edge_removal_filter=[1.0, 0.0], + ).to_scene_graph(t=0) + + scene_dict2 = { + Node(nte.PEDESTRIAN, node_id="1"): np.array([1, 0]), + Node(nte.PEDESTRIAN, node_id="2"): np.array([10, 1]), + Node(nte.PEDESTRIAN, node_id="3"): np.array([20, 1]), + } + sg2 = TemporalSceneGraph.create_from_temp_scene_dict( + scene_dict2, + attention_radius=attention_radius, + duration=1, + edge_addition_filter=[0.25, 0.5, 0.75, 1.0], + edge_removal_filter=[1.0, 0.0], + ).to_scene_graph(t=0) + + new_nodes, removed_nodes, new_neighbors, removed_neighbors = sg2 - sg1 + print("New Nodes:", new_nodes) + print("Removed Nodes:", removed_nodes) + print("New Neighbors:", new_neighbors) + print("Removed Neighbors:", removed_neighbors) + + # # # # # # # # # # # # # # # + print("\n" + "-" * 40 + "\n") + + scene_dict1 = { + Node(nte.PEDESTRIAN, node_id="1"): np.array([0, 0]), + Node(nte.PEDESTRIAN, node_id="2"): np.array([0, 1]), + } + sg1 = TemporalSceneGraph.create_from_temp_scene_dict( + scene_dict1, + attention_radius=attention_radius, + duration=1, + edge_addition_filter=[0.25, 0.5, 0.75, 1.0], + edge_removal_filter=[1.0, 0.0], + ).to_scene_graph(t=0) + + scene_dict2 = { + Node(nte.PEDESTRIAN, node_id="2"): np.array([10, 1]), + Node(nte.PEDESTRIAN, node_id="3"): np.array([12, 1]), + Node(nte.PEDESTRIAN, node_id="4"): np.array([13, 1]), + } + sg2 = TemporalSceneGraph.create_from_temp_scene_dict( + scene_dict2, + attention_radius=attention_radius, + duration=1, + edge_addition_filter=[0.25, 0.5, 0.75, 1.0], + edge_removal_filter=[1.0, 0.0], + ).to_scene_graph(t=0) + + new_nodes, removed_nodes, new_neighbors, removed_neighbors = sg2 - sg1 + print("New Nodes:", new_nodes) + print("Removed Nodes:", removed_nodes) + print("New Neighbors:", new_neighbors) + print("Removed Neighbors:", removed_neighbors) diff --git a/forge/test/models/pytorch/multimodal/trajectron/trajectron/evaluation/__init__.py b/forge/test/models/pytorch/multimodal/trajectron/trajectron/evaluation/__init__.py new file mode 100644 index 000000000..91ce29390 --- /dev/null +++ b/forge/test/models/pytorch/multimodal/trajectron/trajectron/evaluation/__init__.py @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: (c) 2024 Tenstorrent AI ULC +# +# SPDX-License-Identifier: Apache-2.0 +from .evaluation import compute_batch_statistics, log_batch_errors, print_batch_errors diff --git a/forge/test/models/pytorch/multimodal/trajectron/trajectron/evaluation/evaluation.py b/forge/test/models/pytorch/multimodal/trajectron/trajectron/evaluation/evaluation.py new file mode 100644 index 000000000..fac4a45eb --- /dev/null +++ b/forge/test/models/pytorch/multimodal/trajectron/trajectron/evaluation/evaluation.py @@ -0,0 +1,142 @@ +# SPDX-FileCopyrightText: (c) 2024 Tenstorrent AI ULC +# +# SPDX-License-Identifier: Apache-2.0 +import numpy as np +from scipy.interpolate import RectBivariateSpline +from scipy.ndimage import binary_dilation +from scipy.stats import gaussian_kde +from utils import prediction_output_to_trajectories +import visualization +from matplotlib import pyplot as plt + + +def compute_ade(predicted_trajs, gt_traj): + error = np.linalg.norm(predicted_trajs - gt_traj, axis=-1) + ade = np.mean(error, axis=-1) + return ade.flatten() + + +def compute_fde(predicted_trajs, gt_traj): + final_error = np.linalg.norm(predicted_trajs[:, :, -1] - gt_traj[-1], axis=-1) + return final_error.flatten() + + +def compute_kde_nll(predicted_trajs, gt_traj): + kde_ll = 0.0 + log_pdf_lower_bound = -20 + num_timesteps = gt_traj.shape[0] + num_batches = predicted_trajs.shape[0] + + for batch_num in range(num_batches): + for timestep in range(num_timesteps): + try: + kde = gaussian_kde(predicted_trajs[batch_num, :, timestep].T) + pdf = np.clip(kde.logpdf(gt_traj[timestep].T), a_min=log_pdf_lower_bound, a_max=None)[0] + kde_ll += pdf / (num_timesteps * num_batches) + except np.linalg.LinAlgError: + kde_ll = np.nan + + return -kde_ll + + +def compute_obs_violations(predicted_trajs, map): + obs_map = map.data + + interp_obs_map = RectBivariateSpline( + range(obs_map.shape[1]), range(obs_map.shape[0]), binary_dilation(obs_map.T, iterations=4), kx=1, ky=1 + ) + + old_shape = predicted_trajs.shape + pred_trajs_map = map.to_map_points(predicted_trajs.reshape((-1, 2))) + + traj_obs_values = interp_obs_map(pred_trajs_map[:, 0], pred_trajs_map[:, 1], grid=False) + traj_obs_values = traj_obs_values.reshape((old_shape[0], old_shape[1])) + num_viol_trajs = np.sum(traj_obs_values.max(axis=1) > 0, dtype=float) + + return num_viol_trajs + + +def compute_batch_statistics( + prediction_output_dict, + dt, + max_hl, + ph, + node_type_enum, + kde=True, + obs=False, + map=None, + prune_ph_to_future=False, + best_of=False, +): + + (prediction_dict, _, futures_dict) = prediction_output_to_trajectories( + prediction_output_dict, dt, max_hl, ph, prune_ph_to_future=prune_ph_to_future + ) + + batch_error_dict = dict() + for node_type in node_type_enum: + batch_error_dict[node_type] = {"ade": list(), "fde": list(), "kde": list(), "obs_viols": list()} + + for t in prediction_dict.keys(): + for node in prediction_dict[t].keys(): + ade_errors = compute_ade(prediction_dict[t][node], futures_dict[t][node]) + fde_errors = compute_fde(prediction_dict[t][node], futures_dict[t][node]) + if kde: + kde_ll = compute_kde_nll(prediction_dict[t][node], futures_dict[t][node]) + else: + kde_ll = 0 + if obs: + obs_viols = compute_obs_violations(prediction_dict[t][node], map) + else: + obs_viols = 0 + if best_of: + ade_errors = np.min(ade_errors, keepdims=True) + fde_errors = np.min(fde_errors, keepdims=True) + kde_ll = np.min(kde_ll) + batch_error_dict[node.type]["ade"].extend(list(ade_errors)) + batch_error_dict[node.type]["fde"].extend(list(fde_errors)) + batch_error_dict[node.type]["kde"].extend([kde_ll]) + batch_error_dict[node.type]["obs_viols"].extend([obs_viols]) + + return batch_error_dict + + +def log_batch_errors(batch_errors_list, log_writer, namespace, curr_iter, bar_plot=[], box_plot=[]): + for node_type in batch_errors_list[0].keys(): + for metric in batch_errors_list[0][node_type].keys(): + metric_batch_error = [] + for batch_errors in batch_errors_list: + metric_batch_error.extend(batch_errors[node_type][metric]) + + if len(metric_batch_error) > 0: + log_writer.add_histogram(f"{node_type.name}/{namespace}/{metric}", metric_batch_error, curr_iter) + log_writer.add_scalar( + f"{node_type.name}/{namespace}/{metric}_mean", np.mean(metric_batch_error), curr_iter + ) + log_writer.add_scalar( + f"{node_type.name}/{namespace}/{metric}_median", np.median(metric_batch_error), curr_iter + ) + + if metric in bar_plot: + pd = {"dataset": [namespace] * len(metric_batch_error), metric: metric_batch_error} + kde_barplot_fig, ax = plt.subplots(figsize=(5, 5)) + visualization.visualization_utils.plot_barplots(ax, pd, "dataset", metric) + log_writer.add_figure(f"{node_type.name}/{namespace}/{metric}_bar_plot", kde_barplot_fig, curr_iter) + + if metric in box_plot: + mse_fde_pd = {"dataset": [namespace] * len(metric_batch_error), metric: metric_batch_error} + fig, ax = plt.subplots(figsize=(5, 5)) + visualization.visualization_utils.plot_boxplots(ax, mse_fde_pd, "dataset", metric) + log_writer.add_figure(f"{node_type.name}/{namespace}/{metric}_box_plot", fig, curr_iter) + + +def print_batch_errors(batch_errors_list, namespace, curr_iter): + for node_type in batch_errors_list[0].keys(): + for metric in batch_errors_list[0][node_type].keys(): + metric_batch_error = [] + for batch_errors in batch_errors_list: + metric_batch_error.extend(batch_errors[node_type][metric]) + + if len(metric_batch_error) > 0: + print(f"{curr_iter}: {node_type.name}/{namespace}/{metric}_mean", np.mean(metric_batch_error)) + print(f"{curr_iter}: {node_type.name}/{namespace}/{metric}_median", np.median(metric_batch_error)) diff --git a/forge/test/models/pytorch/multimodal/trajectron/trajectron/model/__init__.py b/forge/test/models/pytorch/multimodal/trajectron/trajectron/model/__init__.py new file mode 100644 index 000000000..be76653b0 --- /dev/null +++ b/forge/test/models/pytorch/multimodal/trajectron/trajectron/model/__init__.py @@ -0,0 +1,5 @@ +# SPDX-FileCopyrightText: (c) 2024 Tenstorrent AI ULC +# +# SPDX-License-Identifier: Apache-2.0 +from model.trajectron import Trajectron +from model.mgcvae import MultimodalGenerativeCVAE diff --git a/forge/test/models/pytorch/multimodal/trajectron/trajectron/model/components/__init__.py b/forge/test/models/pytorch/multimodal/trajectron/trajectron/model/components/__init__.py new file mode 100644 index 000000000..ebf3ee86c --- /dev/null +++ b/forge/test/models/pytorch/multimodal/trajectron/trajectron/model/components/__init__.py @@ -0,0 +1,7 @@ +# SPDX-FileCopyrightText: (c) 2024 Tenstorrent AI ULC +# +# SPDX-License-Identifier: Apache-2.0 +from .discrete_latent import DiscreteLatent +from .gmm2d import GMM2D +from .map_encoder import CNNMapEncoder +from .additive_attention import AdditiveAttention, TemporallyBatchedAdditiveAttention diff --git a/forge/test/models/pytorch/multimodal/trajectron/trajectron/model/components/additive_attention.py b/forge/test/models/pytorch/multimodal/trajectron/trajectron/model/components/additive_attention.py new file mode 100644 index 000000000..0d1ec7f2d --- /dev/null +++ b/forge/test/models/pytorch/multimodal/trajectron/trajectron/model/components/additive_attention.py @@ -0,0 +1,71 @@ +# SPDX-FileCopyrightText: (c) 2024 Tenstorrent AI ULC +# +# SPDX-License-Identifier: Apache-2.0 +import torch +import torch.nn as nn +import torch.nn.functional as F + + +class AdditiveAttention(nn.Module): + # Implementing the attention module of Bahdanau et al. 2015 where + # score(h_j, s_(i-1)) = v . tanh(W_1 h_j + W_2 s_(i-1)) + def __init__(self, encoder_hidden_state_dim, decoder_hidden_state_dim, internal_dim=None): + super(AdditiveAttention, self).__init__() + + if internal_dim is None: + internal_dim = int((encoder_hidden_state_dim + decoder_hidden_state_dim) / 2) + + self.w1 = nn.Linear(encoder_hidden_state_dim, internal_dim, bias=False) + self.w2 = nn.Linear(decoder_hidden_state_dim, internal_dim, bias=False) + self.v = nn.Linear(internal_dim, 1, bias=False) + + def score(self, encoder_state, decoder_state): + # encoder_state is of shape (batch, enc_dim) + # decoder_state is of shape (batch, dec_dim) + # return value should be of shape (batch, 1) + return self.v(torch.tanh(self.w1(encoder_state) + self.w2(decoder_state))) + + def forward(self, encoder_states, decoder_state): + # encoder_states is of shape (batch, num_enc_states, enc_dim) + # decoder_state is of shape (batch, dec_dim) + score_vec = torch.cat( + [self.score(encoder_states[:, i], decoder_state) for i in range(encoder_states.shape[1])], dim=1 + ) + # score_vec is of shape (batch, num_enc_states) + + attention_probs = torch.unsqueeze(F.softmax(score_vec, dim=1), dim=2) + # attention_probs is of shape (batch, num_enc_states, 1) + + final_context_vec = torch.sum(attention_probs * encoder_states, dim=1) + # final_context_vec is of shape (batch, enc_dim) + + return final_context_vec, attention_probs + + +class TemporallyBatchedAdditiveAttention(AdditiveAttention): + # Implementing the attention module of Bahdanau et al. 2015 where + # score(h_j, s_(i-1)) = v . tanh(W_1 h_j + W_2 s_(i-1)) + def __init__(self, encoder_hidden_state_dim, decoder_hidden_state_dim, internal_dim=None): + super(TemporallyBatchedAdditiveAttention, self).__init__( + encoder_hidden_state_dim, decoder_hidden_state_dim, internal_dim + ) + + def score(self, encoder_state, decoder_state): + # encoder_state is of shape (batch, num_enc_states, max_time, enc_dim) + # decoder_state is of shape (batch, max_time, dec_dim) + # return value should be of shape (batch, num_enc_states, max_time, 1) + return self.v(torch.tanh(self.w1(encoder_state) + torch.unsqueeze(self.w2(decoder_state), dim=1))) + + def forward(self, encoder_states, decoder_state): + # encoder_states is of shape (batch, num_enc_states, max_time, enc_dim) + # decoder_state is of shape (batch, max_time, dec_dim) + score_vec = self.score(encoder_states, decoder_state) + # score_vec is of shape (batch, num_enc_states, max_time, 1) + + attention_probs = F.softmax(score_vec, dim=1) + # attention_probs is of shape (batch, num_enc_states, max_time, 1) + + final_context_vec = torch.sum(attention_probs * encoder_states, dim=1) + # final_context_vec is of shape (batch, max_time, enc_dim) + + return final_context_vec, torch.squeeze(torch.transpose(attention_probs, 1, 2), dim=3) diff --git a/forge/test/models/pytorch/multimodal/trajectron/trajectron/model/components/discrete_latent.py b/forge/test/models/pytorch/multimodal/trajectron/trajectron/model/components/discrete_latent.py new file mode 100644 index 000000000..222d826b1 --- /dev/null +++ b/forge/test/models/pytorch/multimodal/trajectron/trajectron/model/components/discrete_latent.py @@ -0,0 +1,122 @@ +# SPDX-FileCopyrightText: (c) 2024 Tenstorrent AI ULC +# +# SPDX-License-Identifier: Apache-2.0 +import torch +import torch.distributions as td +import numpy as np +from ..model_utils import ModeKeys + + +class DiscreteLatent(object): + def __init__(self, hyperparams, device): + self.hyperparams = hyperparams + self.z_dim = hyperparams["N"] * hyperparams["K"] + self.N = hyperparams["N"] + self.K = hyperparams["K"] + self.kl_min = hyperparams["kl_min"] + self.device = device + self.temp = None # filled in by MultimodalGenerativeCVAE.set_annealing_params + self.z_logit_clip = None # filled in by MultimodalGenerativeCVAE.set_annealing_params + self.p_dist = None # filled in by MultimodalGenerativeCVAE.encoder + self.q_dist = None # filled in by MultimodalGenerativeCVAE.encoder + + def dist_from_h(self, h, mode): + logits_separated = torch.reshape(h, (-1, self.N, self.K)) + logits_separated_mean_zero = logits_separated - torch.mean(logits_separated, dim=-1, keepdim=True) + if self.z_logit_clip is not None and mode == ModeKeys.TRAIN: + c = self.z_logit_clip + logits = torch.clamp(logits_separated_mean_zero, min=-c, max=c) + else: + logits = logits_separated_mean_zero + + return td.OneHotCategorical(logits=logits) + + def sample_q(self, num_samples, mode): + bs = self.p_dist.probs.size()[0] + num_components = self.N * self.K + z_NK = ( + torch.from_numpy(self.all_one_hot_combinations(self.N, self.K)) + .float() + .to(self.device) + .repeat(num_samples, bs) + ) + return torch.reshape(z_NK, (num_samples * num_components, -1, self.z_dim)) + + def sample_p(self, num_samples, mode, most_likely_z=False, full_dist=True, all_z_sep=False): + num_components = 1 + if full_dist: + bs = self.p_dist.probs.size()[0] + z_NK = ( + torch.from_numpy(self.all_one_hot_combinations(self.N, self.K)) + .float() + .to(self.device) + .repeat(num_samples, bs) + ) + num_components = self.K**self.N + k = num_samples * num_components + elif all_z_sep: + bs = self.p_dist.probs.size()[0] + z_NK = torch.from_numpy(self.all_one_hot_combinations(self.N, self.K)).float().to(self.device).repeat(1, bs) + k = self.K**self.N + num_samples = k + elif most_likely_z: + # Sampling the most likely z from p(z|x). + eye_mat = torch.eye(self.p_dist.event_shape[-1], device=self.device) + argmax_idxs = torch.argmax(self.p_dist.probs, dim=2) + z_NK = torch.unsqueeze(eye_mat[argmax_idxs], dim=0).expand(num_samples, -1, -1, -1) + k = num_samples + else: + z_NK = self.p_dist.sample((num_samples,)) + k = num_samples + + if mode == ModeKeys.PREDICT: + return torch.reshape(z_NK, (k, -1, self.N * self.K)), num_samples, num_components + else: + return torch.reshape(z_NK, (k, -1, self.N * self.K)) + + def kl_q_p(self, log_writer=None, prefix=None, curr_iter=None): + kl_separated = td.kl_divergence(self.q_dist, self.p_dist) + if len(kl_separated.size()) < 2: + kl_separated = torch.unsqueeze(kl_separated, dim=0) + + kl_minibatch = torch.mean(kl_separated, dim=0, keepdim=True) + + if log_writer is not None: + log_writer.add_scalar(prefix + "/true_kl", torch.sum(kl_minibatch), curr_iter) + + if self.kl_min > 0: + kl_lower_bounded = torch.clamp(kl_minibatch, min=self.kl_min) + kl = torch.sum(kl_lower_bounded) + else: + kl = torch.sum(kl_minibatch) + + return kl + + def q_log_prob(self, z): + k = z.size()[0] + z_NK = torch.reshape(z, [k, -1, self.N, self.K]) + return torch.sum(self.q_dist.log_prob(z_NK), dim=2) + + def p_log_prob(self, z): + k = z.size()[0] + z_NK = torch.reshape(z, [k, -1, self.N, self.K]) + return torch.sum(self.p_dist.log_prob(z_NK), dim=2) + + def get_p_dist_probs(self): + return self.p_dist.probs + + @staticmethod + def all_one_hot_combinations(N, K): + return np.eye(K).take(np.reshape(np.indices([K] * N), [N, -1]).T, axis=0).reshape(-1, N * K) # [K**N, N*K] + + def summarize_for_tensorboard(self, log_writer, prefix, curr_iter): + log_writer.add_histogram(prefix + "/latent/p_z_x", self.p_dist.probs, curr_iter) + log_writer.add_histogram(prefix + "/latent/q_z_xy", self.q_dist.probs, curr_iter) + log_writer.add_histogram(prefix + "/latent/p_z_x_logits", self.p_dist.logits, curr_iter) + log_writer.add_histogram(prefix + "/latent/q_z_xy_logits", self.q_dist.logits, curr_iter) + if self.z_dim <= 9: + for i in range(self.N): + for j in range(self.K): + log_writer.add_histogram( + prefix + "/latent/q_z_xy_logit{0}{1}".format(i, j), self.q_dist.logits[:, i, j], curr_iter + ) diff --git a/forge/test/models/pytorch/multimodal/trajectron/trajectron/model/components/gmm2d.py b/forge/test/models/pytorch/multimodal/trajectron/trajectron/model/components/gmm2d.py new file mode 100644 index 000000000..999c0a303 --- /dev/null +++ b/forge/test/models/pytorch/multimodal/trajectron/trajectron/model/components/gmm2d.py @@ -0,0 +1,187 @@ +# SPDX-FileCopyrightText: (c) 2024 Tenstorrent AI ULC +# +# SPDX-License-Identifier: Apache-2.0 +import torch +import torch.distributions as td +import numpy as np +from ..model_utils import to_one_hot + + +class GMM2D(td.Distribution): + r""" + Gaussian Mixture Model using 2D Multivariate Gaussians each of as N components: + Cholesky decompesition and affine transformation for sampling: + + .. math:: Z \sim N(0, I) + + .. math:: S = \mu + LZ + + .. math:: S \sim N(\mu, \Sigma) \rightarrow N(\mu, LL^T) + + where :math:`L = chol(\Sigma)` and + + .. math:: \Sigma = \left[ {\begin{array}{cc} \sigma^2_x & \rho \sigma_x \sigma_y \\ \rho \sigma_x \sigma_y & \sigma^2_y \\ \end{array} } \right] + + such that + + .. math:: L = chol(\Sigma) = \left[ {\begin{array}{cc} \sigma_x & 0 \\ \rho \sigma_y & \sigma_y \sqrt{1-\rho^2} \\ \end{array} } \right] + + :param log_pis: Log Mixing Proportions :math:`log(\pi)`. [..., N] + :param mus: Mixture Components mean :math:`\mu`. [..., N * 2] + :param log_sigmas: Log Standard Deviations :math:`log(\sigma_d)`. [..., N * 2] + :param corrs: Cholesky factor of correlation :math:`\rho`. [..., N] + :param clip_lo: Clips the lower end of the standard deviation. + :param clip_hi: Clips the upper end of the standard deviation. + """ + + def __init__(self, log_pis, mus, log_sigmas, corrs): + super(GMM2D, self).__init__(batch_shape=log_pis.shape[0], event_shape=log_pis.shape[1:]) + self.components = log_pis.shape[-1] + self.dimensions = 2 + self.device = log_pis.device + + log_pis = torch.clamp(log_pis, min=-1e5) + self.log_pis = log_pis - torch.logsumexp(log_pis, dim=-1, keepdim=True) # [..., N] + self.mus = self.reshape_to_components(mus) # [..., N, 2] + self.log_sigmas = self.reshape_to_components(log_sigmas) # [..., N, 2] + self.sigmas = torch.exp(self.log_sigmas) # [..., N, 2] + self.one_minus_rho2 = 1 - corrs**2 # [..., N] + self.one_minus_rho2 = torch.clamp(self.one_minus_rho2, min=1e-5, max=1) # otherwise log can be nan + self.corrs = corrs # [..., N] + + self.L = torch.stack( + [ + torch.stack([self.sigmas[..., 0], torch.zeros_like(self.log_pis)], dim=-1), + torch.stack( + [self.sigmas[..., 1] * self.corrs, self.sigmas[..., 1] * torch.sqrt(self.one_minus_rho2)], dim=-1 + ), + ], + dim=-2, + ) + + self.pis_cat_dist = td.Categorical(logits=log_pis) + + @classmethod + def from_log_pis_mus_cov_mats(cls, log_pis, mus, cov_mats): + corrs_sigma12 = cov_mats[..., 0, 1] + sigma_1 = torch.clamp(cov_mats[..., 0, 0], min=1e-8) + sigma_2 = torch.clamp(cov_mats[..., 1, 1], min=1e-8) + sigmas = torch.stack([torch.sqrt(sigma_1), torch.sqrt(sigma_2)], dim=-1) + log_sigmas = torch.log(sigmas) + corrs = corrs_sigma12 / (torch.prod(sigmas, dim=-1)) + return cls(log_pis, mus, log_sigmas, corrs) + + def rsample(self, sample_shape=torch.Size()): + """ + Generates a sample_shape shaped reparameterized sample or sample_shape + shaped batch of reparameterized samples if the distribution parameters + are batched. + + :param sample_shape: Shape of the samples + :return: Samples from the GMM. + """ + mvn_samples = self.mus + torch.squeeze( + torch.matmul( + self.L, torch.unsqueeze(torch.randn(size=sample_shape + self.mus.shape, device=self.device), dim=-1) + ), + dim=-1, + ) + component_cat_samples = self.pis_cat_dist.sample(sample_shape) + selector = torch.unsqueeze(to_one_hot(component_cat_samples, self.components), dim=-1) + return torch.sum(mvn_samples * selector, dim=-2) + + def log_prob(self, value): + r""" + Calculates the log probability of a value using the PDF for bivariate normal distributions: + + .. math:: + f(x | \mu, \sigma, \rho)={\frac {1}{2\pi \sigma _{x}\sigma _{y}{\sqrt {1-\rho ^{2}}}}}\exp + \left(-{\frac {1}{2(1-\rho ^{2})}}\left[{\frac {(x-\mu _{x})^{2}}{\sigma _{x}^{2}}}+ + {\frac {(y-\mu _{y})^{2}}{\sigma _{y}^{2}}}-{\frac {2\rho (x-\mu _{x})(y-\mu _{y})} + {\sigma _{x}\sigma _{y}}}\right]\right) + + :param value: The log probability density function is evaluated at those values. + :return: Log probability + """ + # x: [..., 2] + value = torch.unsqueeze(value, dim=-2) # [..., 1, 2] + dx = value - self.mus # [..., N, 2] + + exp_nominator = torch.sum( + (dx / self.sigmas) ** 2, dim=-1 + ) - 2 * self.corrs * torch.prod( # first and second term of exp nominator + dx, dim=-1 + ) / torch.prod( + self.sigmas, dim=-1 + ) # [..., N] + + component_log_p = ( + -( + 2 * np.log(2 * np.pi) + + torch.log(self.one_minus_rho2) + + 2 * torch.sum(self.log_sigmas, dim=-1) + + exp_nominator / self.one_minus_rho2 + ) + / 2 + ) + + return torch.logsumexp(self.log_pis + component_log_p, dim=-1) + + def get_for_node_at_time(self, n, t): + return self.__class__( + self.log_pis[:, n : n + 1, t : t + 1], + self.mus[:, n : n + 1, t : t + 1], + self.log_sigmas[:, n : n + 1, t : t + 1], + self.corrs[:, n : n + 1, t : t + 1], + ) + + def mode(self): + """ + Calculates the mode of the GMM by calculating probabilities of a 2D mesh grid + + :param required_accuracy: Accuracy of the meshgrid + :return: Mode of the GMM + """ + if self.mus.shape[-2] > 1: + samp, bs, time, comp, _ = self.mus.shape + assert samp == 1, "For taking the mode only one sample makes sense." + mode_node_list = [] + for n in range(bs): + mode_t_list = [] + for t in range(time): + nt_gmm = self.get_for_node_at_time(n, t) + x_min = self.mus[:, n, t, :, 0].min() + x_max = self.mus[:, n, t, :, 0].max() + y_min = self.mus[:, n, t, :, 1].min() + y_max = self.mus[:, n, t, :, 1].max() + search_grid = ( + torch.stack( + torch.meshgrid([torch.arange(x_min, x_max, 0.01), torch.arange(y_min, y_max, 0.01)]), dim=2 + ) + .view(-1, 2) + .float() + .to(self.device) + ) + + ll_score = nt_gmm.log_prob(search_grid) + argmax = torch.argmax(ll_score.squeeze(), dim=0) + mode_t_list.append(search_grid[argmax]) + mode_node_list.append(torch.stack(mode_t_list, dim=0)) + return torch.stack(mode_node_list, dim=0).unsqueeze(dim=0) + return torch.squeeze(self.mus, dim=-2) + + def reshape_to_components(self, tensor): + if len(tensor.shape) == 5: + return tensor + return torch.reshape(tensor, list(tensor.shape[:-1]) + [self.components, self.dimensions]) + + def get_covariance_matrix(self): + cov = self.corrs * torch.prod(self.sigmas, dim=-1) + E = torch.stack( + [ + torch.stack([self.sigmas[..., 0] ** 2, cov], dim=-1), + torch.stack([cov, self.sigmas[..., 1] ** 2], dim=-1), + ], + dim=-2, + ) + return E diff --git a/forge/test/models/pytorch/multimodal/trajectron/trajectron/model/components/graph_attention.py b/forge/test/models/pytorch/multimodal/trajectron/trajectron/model/components/graph_attention.py new file mode 100644 index 000000000..6c9516753 --- /dev/null +++ b/forge/test/models/pytorch/multimodal/trajectron/trajectron/model/components/graph_attention.py @@ -0,0 +1,61 @@ +# SPDX-FileCopyrightText: (c) 2024 Tenstorrent AI ULC +# +# SPDX-License-Identifier: Apache-2.0 +import warnings +import math +import numbers +import torch +import torch.nn as nn +import torch.nn.functional as F +from torch.nn import init, Parameter + + +class GraphMultiTypeAttention(nn.Module): + def __init__(self, in_features, hidden_features, out_features, bias=True, types=1): + super(GraphMultiTypeAttention, self).__init__() + self.types = types + self.in_features = in_features + self.out_features = out_features + self.node_self_loop_weight = Parameter(torch.Tensor(hidden_features, in_features[0])) + + self.weight_per_type = nn.ParameterList() + for i in range(types): + self.weight_per_type.append(Parameter(torch.Tensor(hidden_features, in_features[i]))) + if bias: + self.bias = Parameter(torch.Tensor(hidden_features)) + else: + self.register_parameter("bias", None) + + self.linear_to_out = nn.Linear(hidden_features, out_features, bias=bias) + + self.reset_parameters() + + def reset_parameters(self): + for weight in self.weight_per_type: + bound = 1 / math.sqrt(weight.size(1)) + init.uniform_(weight, -bound, bound) + bound = 1 / math.sqrt(self.node_self_loop_weight.size(1)) + init.uniform_(self.node_self_loop_weight, -bound, bound) + if self.bias is not None: + init.uniform_(self.bias, -bound, bound) + + def forward(self, inputs, types, edge_weights): + weight_list = list() + for i, type in enumerate(types): + weight_list.append((edge_weights[i] / len(edge_weights)) * self.weight_per_type[type].T) + weight_list.append(self.node_self_loop_weight.T) + weight = torch.cat(weight_list, dim=0) + stacked_input = torch.cat(inputs, dim=-1) + output = stacked_input.matmul(weight) + + output = output + + if self.bias is not None: + output += self.bias + + return torch.relu(self.linear_to_out(torch.relu(output))) + + def extra_repr(self): + return "in_features={}, hidden_features={},, out_features={}, types={}, bias={}".format( + self.in_features, self.hidden_features, self.out_features, self.types, self.bias is not None + ) diff --git a/forge/test/models/pytorch/multimodal/trajectron/trajectron/model/components/map_encoder.py b/forge/test/models/pytorch/multimodal/trajectron/trajectron/model/components/map_encoder.py new file mode 100644 index 000000000..369be7db4 --- /dev/null +++ b/forge/test/models/pytorch/multimodal/trajectron/trajectron/model/components/map_encoder.py @@ -0,0 +1,33 @@ +# SPDX-FileCopyrightText: (c) 2024 Tenstorrent AI ULC +# +# SPDX-License-Identifier: Apache-2.0 +import torch +import torch.nn as nn +import torch.nn.functional as F + + +class CNNMapEncoder(nn.Module): + def __init__(self, map_channels, hidden_channels, output_size, masks, strides, patch_size): + super(CNNMapEncoder, self).__init__() + self.convs = nn.ModuleList() + patch_size_x = patch_size[0] + patch_size[2] + patch_size_y = patch_size[1] + patch_size[3] + input_size = (map_channels, patch_size_x, patch_size_y) + x_dummy = torch.ones(input_size).unsqueeze(0) * torch.tensor(float("nan")) + + for i, hidden_size in enumerate(hidden_channels): + self.convs.append( + nn.Conv2d( + map_channels if i == 0 else hidden_channels[i - 1], hidden_channels[i], masks[i], stride=strides[i] + ) + ) + x_dummy = self.convs[i](x_dummy) + + self.fc = nn.Linear(x_dummy.numel(), output_size) + + def forward(self, x, training): + for conv in self.convs: + x = F.leaky_relu(conv(x), 0.2) + x = torch.flatten(x, start_dim=1) + x = self.fc(x) + return x diff --git a/forge/test/models/pytorch/multimodal/trajectron/trajectron/model/dataset/__init__.py b/forge/test/models/pytorch/multimodal/trajectron/trajectron/model/dataset/__init__.py new file mode 100644 index 000000000..e0d2ee4c0 --- /dev/null +++ b/forge/test/models/pytorch/multimodal/trajectron/trajectron/model/dataset/__init__.py @@ -0,0 +1,5 @@ +# SPDX-FileCopyrightText: (c) 2024 Tenstorrent AI ULC +# +# SPDX-License-Identifier: Apache-2.0 +from .dataset import EnvironmentDataset, NodeTypeDataset +from .preprocessing import collate, get_node_timestep_data, get_timesteps_data, restore, get_relative_robot_traj diff --git a/forge/test/models/pytorch/multimodal/trajectron/trajectron/model/dataset/dataset.py b/forge/test/models/pytorch/multimodal/trajectron/trajectron/model/dataset/dataset.py new file mode 100644 index 000000000..4769eae88 --- /dev/null +++ b/forge/test/models/pytorch/multimodal/trajectron/trajectron/model/dataset/dataset.py @@ -0,0 +1,95 @@ +# SPDX-FileCopyrightText: (c) 2024 Tenstorrent AI ULC +# +# SPDX-License-Identifier: Apache-2.0 +from torch.utils import data +import numpy as np +from .preprocessing import get_node_timestep_data + + +class EnvironmentDataset(object): + def __init__(self, env, state, pred_state, node_freq_mult, scene_freq_mult, hyperparams, **kwargs): + self.env = env + self.state = state + self.pred_state = pred_state + self.hyperparams = hyperparams + self.max_ht = self.hyperparams["maximum_history_length"] + self.max_ft = kwargs["min_future_timesteps"] + self.node_type_datasets = list() + self._augment = False + for node_type in env.NodeType: + if node_type not in hyperparams["pred_state"]: + continue + self.node_type_datasets.append( + NodeTypeDataset( + env, node_type, state, pred_state, node_freq_mult, scene_freq_mult, hyperparams, **kwargs + ) + ) + + @property + def augment(self): + return self._augment + + @augment.setter + def augment(self, value): + self._augment = value + for node_type_dataset in self.node_type_datasets: + node_type_dataset.augment = value + + def __iter__(self): + return iter(self.node_type_datasets) + + +class NodeTypeDataset(data.Dataset): + def __init__( + self, env, node_type, state, pred_state, node_freq_mult, scene_freq_mult, hyperparams, augment=False, **kwargs + ): + self.env = env + self.state = state + self.pred_state = pred_state + self.hyperparams = hyperparams + self.max_ht = self.hyperparams["maximum_history_length"] + self.max_ft = kwargs["min_future_timesteps"] + + self.augment = augment + + self.node_type = node_type + self.index = self.index_env(node_freq_mult, scene_freq_mult, **kwargs) + self.len = len(self.index) + self.edge_types = [edge_type for edge_type in env.get_edge_types() if edge_type[0] is node_type] + + def index_env(self, node_freq_mult, scene_freq_mult, **kwargs): + index = list() + for scene in self.env.scenes: + present_node_dict = scene.present_nodes(np.arange(0, scene.timesteps), type=self.node_type, **kwargs) + for t, nodes in present_node_dict.items(): + for node in nodes: + index += ( + [(scene, t, node)] + * (scene.frequency_multiplier if scene_freq_mult else 1) + * (node.frequency_multiplier if node_freq_mult else 1) + ) + + return index + + def __len__(self): + return self.len + + def __getitem__(self, i): + (scene, t, node) = self.index[i] + + if self.augment: + scene = scene.augment() + node = scene.get_node_by_id(node.id) + + return get_node_timestep_data( + self.env, + scene, + t, + node, + self.state, + self.pred_state, + self.edge_types, + self.max_ht, + self.max_ft, + self.hyperparams, + ) diff --git a/forge/test/models/pytorch/multimodal/trajectron/trajectron/model/dataset/homography_warper.py b/forge/test/models/pytorch/multimodal/trajectron/trajectron/model/dataset/homography_warper.py new file mode 100644 index 000000000..5cc3a5f4d --- /dev/null +++ b/forge/test/models/pytorch/multimodal/trajectron/trajectron/model/dataset/homography_warper.py @@ -0,0 +1,467 @@ +# SPDX-FileCopyrightText: (c) 2024 Tenstorrent AI ULC +# +# SPDX-License-Identifier: Apache-2.0 +import torch +import torch.nn as nn +import torch.nn.functional as F +from typing import Tuple, Optional + + +pi = torch.tensor(3.14159265358979323846) + + +def deg2rad(tensor: torch.Tensor) -> torch.Tensor: + r"""Function that converts angles from degrees to radians. + Args: + tensor (torch.Tensor): Tensor of arbitrary shape. + Returns: + torch.Tensor: tensor with same shape as input. + """ + if not isinstance(tensor, torch.Tensor): + raise TypeError("Input type is not a torch.Tensor. Got {}".format(type(tensor))) + + return tensor * pi.to(tensor.device).type(tensor.dtype) / 180.0 + + +def angle_to_rotation_matrix(angle: torch.Tensor) -> torch.Tensor: + """ + Creates a rotation matrix out of angles in degrees + Args: + angle: (torch.Tensor): tensor of angles in degrees, any shape. + Returns: + torch.Tensor: tensor of *x2x2 rotation matrices. + Shape: + - Input: :math:`(*)` + - Output: :math:`(*, 2, 2)` + Example: + >>> input = torch.rand(1, 3) # Nx3 + >>> output = kornia.angle_to_rotation_matrix(input) # Nx3x2x2 + """ + ang_rad = deg2rad(angle) + cos_a: torch.Tensor = torch.cos(ang_rad) + sin_a: torch.Tensor = torch.sin(ang_rad) + return torch.stack([cos_a, sin_a, -sin_a, cos_a], dim=-1).view(*angle.shape, 2, 2) + + +def get_rotation_matrix2d(center: torch.Tensor, angle: torch.Tensor, scale: torch.Tensor) -> torch.Tensor: + r"""Calculates an affine matrix of 2D rotation. + The function calculates the following matrix: + .. math:: + \begin{bmatrix} + \alpha & \beta & (1 - \alpha) \cdot \text{x} + - \beta \cdot \text{y} \\ + -\beta & \alpha & \beta \cdot \text{x} + + (1 - \alpha) \cdot \text{y} + \end{bmatrix} + where + .. math:: + \alpha = \text{scale} \cdot cos(\text{radian}) \\ + \beta = \text{scale} \cdot sin(\text{radian}) + The transformation maps the rotation center to itself + If this is not the target, adjust the shift. + Args: + center (Tensor): center of the rotation in the source image. + angle (Tensor): rotation radian in degrees. Positive values mean + counter-clockwise rotation (the coordinate origin is assumed to + be the top-left corner). + scale (Tensor): isotropic scale factor. + Returns: + Tensor: the affine matrix of 2D rotation. + Shape: + - Input: :math:`(B, 2)`, :math:`(B)` and :math:`(B)` + - Output: :math:`(B, 2, 3)` + Example: + >>> center = torch.zeros(1, 2) + >>> scale = torch.ones(1) + >>> radian = 45. * torch.ones(1) + >>> M = kornia.get_rotation_matrix2d(center, radian, scale) + tensor([[[ 0.7071, 0.7071, 0.0000], + [-0.7071, 0.7071, 0.0000]]]) + """ + if not torch.is_tensor(center): + raise TypeError("Input center type is not a torch.Tensor. Got {}".format(type(center))) + if not torch.is_tensor(angle): + raise TypeError("Input radian type is not a torch.Tensor. Got {}".format(type(angle))) + if not torch.is_tensor(scale): + raise TypeError("Input scale type is not a torch.Tensor. Got {}".format(type(scale))) + if not (len(center.shape) == 2 and center.shape[1] == 2): + raise ValueError("Input center must be a Bx2 tensor. Got {}".format(center.shape)) + if not len(angle.shape) == 1: + raise ValueError("Input radian must be a B tensor. Got {}".format(angle.shape)) + if not len(scale.shape) == 1: + raise ValueError("Input scale must be a B tensor. Got {}".format(scale.shape)) + if not (center.shape[0] == angle.shape[0] == scale.shape[0]): + raise ValueError( + "Inputs must have same batch size dimension. Got {}".format(center.shape, angle.shape, scale.shape) + ) + # convert radian and apply scale + scaled_rotation: torch.Tensor = angle_to_rotation_matrix(angle) * scale.view(-1, 1, 1) + alpha: torch.Tensor = scaled_rotation[:, 0, 0] + beta: torch.Tensor = scaled_rotation[:, 0, 1] + + # unpack the center to x, y coordinates + x: torch.Tensor = center[..., 0] + y: torch.Tensor = center[..., 1] + + # create output tensor + batch_size: int = center.shape[0] + M: torch.Tensor = torch.zeros(batch_size, 2, 3, device=center.device, dtype=center.dtype) + M[..., 0:2, 0:2] = scaled_rotation + M[..., 0, 2] = (torch.tensor(1.0) - alpha) * x - beta * y + M[..., 1, 2] = beta * x + (torch.tensor(1.0) - alpha) * y + return M + + +def convert_points_to_homogeneous(points: torch.Tensor) -> torch.Tensor: + r"""Function that converts points from Euclidean to homogeneous space. + Examples:: + >>> input = torch.rand(2, 4, 3) # BxNx3 + >>> output = kornia.convert_points_to_homogeneous(input) # BxNx4 + """ + if not isinstance(points, torch.Tensor): + raise TypeError("Input type is not a torch.Tensor. Got {}".format(type(points))) + if len(points.shape) < 2: + raise ValueError("Input must be at least a 2D tensor. Got {}".format(points.shape)) + + return torch.nn.functional.pad(points, [0, 1], "constant", 1.0) + + +def convert_points_from_homogeneous(points: torch.Tensor, eps: float = 1e-8) -> torch.Tensor: + r"""Function that converts points from homogeneous to Euclidean space. + Examples:: + >>> input = torch.rand(2, 4, 3) # BxNx3 + >>> output = kornia.convert_points_from_homogeneous(input) # BxNx2 + """ + if not isinstance(points, torch.Tensor): + raise TypeError("Input type is not a torch.Tensor. Got {}".format(type(points))) + + if len(points.shape) < 2: + raise ValueError("Input must be at least a 2D tensor. Got {}".format(points.shape)) + + # we check for points at infinity + z_vec: torch.Tensor = points[..., -1:] + + # set the results of division by zeror/near-zero to 1.0 + # follow the convention of opencv: + # https://github.com/opencv/opencv/pull/14411/files + mask: torch.Tensor = torch.abs(z_vec) > eps + scale: torch.Tensor = torch.ones_like(z_vec).masked_scatter_( + mask, torch.tensor(1.0).to(points.device) / z_vec[mask] + ) + + return scale * points[..., :-1] + + +def transform_points(trans_01: torch.Tensor, points_1: torch.Tensor) -> torch.Tensor: + r"""Function that applies transformations to a set of points. + Args: + trans_01 (torch.Tensor): tensor for transformations of shape + :math:`(B, D+1, D+1)`. + points_1 (torch.Tensor): tensor of points of shape :math:`(B, N, D)`. + Returns: + torch.Tensor: tensor of N-dimensional points. + Shape: + - Output: :math:`(B, N, D)` + Examples: + >>> points_1 = torch.rand(2, 4, 3) # BxNx3 + >>> trans_01 = torch.eye(4).view(1, 4, 4) # Bx4x4 + >>> points_0 = kornia.transform_points(trans_01, points_1) # BxNx3 + """ + if not torch.is_tensor(trans_01) or not torch.is_tensor(points_1): + raise TypeError("Input type is not a torch.Tensor") + if not trans_01.device == points_1.device: + raise TypeError("Tensor must be in the same device") + if not trans_01.shape[0] == points_1.shape[0] and trans_01.shape[0] != 1: + raise ValueError("Input batch size must be the same for both tensors or 1") + if not trans_01.shape[-1] == (points_1.shape[-1] + 1): + raise ValueError("Last input dimensions must differe by one unit") + # to homogeneous + points_1_h = convert_points_to_homogeneous(points_1) # BxNxD+1 + # transform coordinates + points_0_h = torch.matmul(trans_01.unsqueeze(1), points_1_h.unsqueeze(-1)) + points_0_h = torch.squeeze(points_0_h, dim=-1) + # to euclidean + points_0 = convert_points_from_homogeneous(points_0_h) # BxNxD + return points_0 + + +def multi_linspace(a, b, num, endpoint=True, device="cpu", dtype=torch.float): + """This function is just like np.linspace, but will create linearly + spaced vectors from a start to end vector. + Inputs: + a - Start vector. + b - End vector. + num - Number of samples to generate. Default is 50. Must be above 0. + endpoint - If True, b is the last sample. + Otherwise, it is not included. Default is True. + """ + + return a[..., None] + (b - a)[..., None] / (num - endpoint) * torch.arange(num, device=device, dtype=dtype) + + +def create_batched_meshgrid( + x_min: torch.Tensor, + y_min: torch.Tensor, + x_max: torch.Tensor, + y_max: torch.Tensor, + height: int, + width: int, + device: Optional[torch.device] = torch.device("cpu"), +) -> torch.Tensor: + """Generates a coordinate grid for an image. + When the flag `normalized_coordinates` is set to True, the grid is + normalized to be in the range [-1,1] to be consistent with the pytorch + function grid_sample. + http://pytorch.org/docs/master/nn.html#torch.nn.functional.grid_sample + Args: + height (int): the image height (rows). + width (int): the image width (cols). + normalized_coordinates (Optional[bool]): whether to normalize + coordinates in the range [-1, 1] in order to be consistent with the + PyTorch function grid_sample. + Return: + torch.Tensor: returns a grid tensor with shape :math:`(1, H, W, 2)`. + """ + # generate coordinates + xs = multi_linspace(x_min, x_max, width, device=device, dtype=torch.float) + ys = multi_linspace(y_min, y_max, height, device=device, dtype=torch.float) + + # generate grid by stacking coordinates + bs = x_min.shape[0] + batched_grid_i_list = list() + for i in range(bs): + batched_grid_i_list.append(torch.stack(torch.meshgrid([xs[i], ys[i]])).transpose(1, 2)) # 2xHxW + batched_grid: torch.Tensor = torch.stack(batched_grid_i_list, dim=0) + return batched_grid.permute(0, 2, 3, 1) # BxHxWx2 + + +def homography_warp( + patch_src: torch.Tensor, + centers: torch.Tensor, + dst_homo_src: torch.Tensor, + dsize: Tuple[int, int], + mode: str = "bilinear", + padding_mode: str = "zeros", +) -> torch.Tensor: + r"""Function that warps image patchs or tensors by homographies. + See :class:`~kornia.geometry.warp.HomographyWarper` for details. + Args: + patch_src (torch.Tensor): The image or tensor to warp. Should be from + source of shape :math:`(N, C, H, W)`. + dst_homo_src (torch.Tensor): The homography or stack of homographies + from source to destination of shape + :math:`(N, 3, 3)`. + dsize (Tuple[int, int]): The height and width of the image to warp. + mode (str): interpolation mode to calculate output values + 'bilinear' | 'nearest'. Default: 'bilinear'. + padding_mode (str): padding mode for outside grid values + 'zeros' | 'border' | 'reflection'. Default: 'zeros'. + Return: + torch.Tensor: Patch sampled at locations from source to destination. + Example: + >>> input = torch.rand(1, 3, 32, 32) + >>> homography = torch.eye(3).view(1, 3, 3) + >>> output = kornia.homography_warp(input, homography, (32, 32)) + """ + + out_height, out_width = dsize + image_height, image_width = patch_src.shape[-2:] + x_min = 2.0 * (centers[..., 0] - out_width / 2) / image_width - 1.0 + y_min = 2.0 * (centers[..., 1] - out_height / 2) / image_height - 1.0 + x_max = 2.0 * (centers[..., 0] + out_width / 2) / image_width - 1.0 + y_max = 2.0 * (centers[..., 1] + out_height / 2) / image_height - 1.0 + warper = HomographyWarper(x_min, y_min, x_max, y_max, out_height, out_width, mode, padding_mode) + return warper(patch_src, dst_homo_src) + + +def normal_transform_pixel(height, width): + + tr_mat = torch.Tensor([[1.0, 0.0, -1.0], [0.0, 1.0, -1.0], [0.0, 0.0, 1.0]]) # 1x3x3 + + tr_mat[0, 0] = tr_mat[0, 0] * 2.0 / (width - 1.0) + tr_mat[1, 1] = tr_mat[1, 1] * 2.0 / (height - 1.0) + + tr_mat = tr_mat.unsqueeze(0) + + return tr_mat + + +def src_norm_to_dst_norm( + dst_pix_trans_src_pix: torch.Tensor, dsize_src: Tuple[int, int], dsize_dst: Tuple[int, int] +) -> torch.Tensor: + # source and destination sizes + src_h, src_w = dsize_src + dst_h, dst_w = dsize_dst + # the devices and types + device: torch.device = dst_pix_trans_src_pix.device + dtype: torch.dtype = dst_pix_trans_src_pix.dtype + # compute the transformation pixel/norm for src/dst + src_norm_trans_src_pix: torch.Tensor = normal_transform_pixel(src_h, src_w).to(device, dtype) + src_pix_trans_src_norm = torch.inverse(src_norm_trans_src_pix) + dst_norm_trans_dst_pix: torch.Tensor = normal_transform_pixel(dst_h, dst_w).to(device, dtype) + # compute chain transformations + dst_norm_trans_src_norm: torch.Tensor = dst_norm_trans_dst_pix @ (dst_pix_trans_src_pix @ src_pix_trans_src_norm) + return dst_norm_trans_src_norm + + +def transform_warp_impl( + src: torch.Tensor, + centers: torch.Tensor, + dst_pix_trans_src_pix: torch.Tensor, + dsize_src: Tuple[int, int], + dsize_dst: Tuple[int, int], + grid_mode: str, + padding_mode: str, +) -> torch.Tensor: + """Compute the transform in normalized cooridnates and perform the warping.""" + dst_norm_trans_src_norm: torch.Tensor = src_norm_to_dst_norm(dst_pix_trans_src_pix, dsize_src, dsize_src) + + src_norm_trans_dst_norm = torch.inverse(dst_norm_trans_src_norm) + return homography_warp(src, centers, src_norm_trans_dst_norm, dsize_dst, grid_mode, padding_mode) + + +class HomographyWarper(nn.Module): + r"""Warps image patches or tensors by homographies. + .. math:: + X_{dst} = H_{src}^{\{dst\}} * X_{src} + Args: + height (int): The height of the image to warp. + width (int): The width of the image to warp. + mode (str): interpolation mode to calculate output values + 'bilinear' | 'nearest'. Default: 'bilinear'. + padding_mode (str): padding mode for outside grid values + 'zeros' | 'border' | 'reflection'. Default: 'zeros'. + """ + + def __init__( + self, + x_min: torch.Tensor, + y_min: torch.Tensor, + x_max: torch.Tensor, + y_max: torch.Tensor, + height: int, + width: int, + mode: str = "bilinear", + padding_mode: str = "zeros", + ) -> None: + super(HomographyWarper, self).__init__() + self.width: int = width + self.height: int = height + self.mode: str = mode + self.padding_mode: str = padding_mode + + # create base grid to compute the flow + self.grid: torch.Tensor = create_batched_meshgrid(x_min, y_min, x_max, y_max, height, width) + + def warp_grid(self, dst_homo_src: torch.Tensor) -> torch.Tensor: + r"""Computes the grid to warp the coordinates grid by an homography. + Args: + dst_homo_src (torch.Tensor): Homography or homographies (stacked) to + transform all points in the grid. Shape of the + homography has to be :math:`(N, 3, 3)`. + Returns: + torch.Tensor: the transformed grid of shape :math:`(N, H, W, 2)`. + """ + batch_size: int = dst_homo_src.shape[0] + device: torch.device = dst_homo_src.device + dtype: torch.dtype = dst_homo_src.dtype + # expand grid to match the input batch size + grid: torch.Tensor = self.grid + if len(dst_homo_src.shape) == 3: # local homography case + dst_homo_src = dst_homo_src.view(batch_size, 1, 3, 3) # NxHxWx3x3 + # perform the actual grid transformation, + # the grid is copied to input device and casted to the same type + flow: torch.Tensor = transform_points(dst_homo_src, grid.to(device).to(dtype)) # NxHxWx2 + return flow.view(batch_size, self.height, self.width, 2) # NxHxWx2 + + def forward(self, patch_src: torch.Tensor, dst_homo_src: torch.Tensor) -> torch.Tensor: # type: ignore + r"""Warps an image or tensor from source into reference frame. + Args: + patch_src (torch.Tensor): The image or tensor to warp. + Should be from source. + dst_homo_src (torch.Tensor): The homography or stack of homographies + from source to destination. The homography assumes normalized + coordinates [-1, 1]. + Return: + torch.Tensor: Patch sampled at locations from source to destination. + Shape: + - Input: :math:`(N, C, H, W)` and :math:`(N, 3, 3)` + - Output: :math:`(N, C, H, W)` + Example: + >>> input = torch.rand(1, 3, 32, 32) + >>> homography = torch.eye(3).view(1, 3, 3) + >>> warper = kornia.HomographyWarper(32, 32) + >>> output = warper(input, homography) # NxCxHxW + """ + if not dst_homo_src.device == patch_src.device: + raise TypeError( + "Patch and homography must be on the same device. \ + Got patch.device: {} dst_H_src.device: {}.".format( + patch_src.device, dst_homo_src.device + ) + ) + + return F.grid_sample( + patch_src, + self.warp_grid(dst_homo_src), # type: ignore + mode=self.mode, + padding_mode=self.padding_mode, + align_corners=True, + ) + + +def warp_affine_crop( + src: torch.Tensor, + centers: torch.Tensor, + M: torch.Tensor, + dsize: Tuple[int, int], + flags: str = "bilinear", + padding_mode: str = "zeros", +) -> torch.Tensor: + r"""Applies an affine transformation to a tensor. + + The function warp_affine transforms the source tensor using + the specified matrix: + + .. math:: + \text{dst}(x, y) = \text{src} \left( M_{11} x + M_{12} y + M_{13} , + M_{21} x + M_{22} y + M_{23} \right ) + + Args: + src (torch.Tensor): input tensor of shape :math:`(B, C, H, W)`. + M (torch.Tensor): affine transformation of shape :math:`(B, 2, 3)`. + dsize (Tuple[int, int]): size of the output image (height, width). + mode (str): interpolation mode to calculate output values + 'bilinear' | 'nearest'. Default: 'bilinear'. + padding_mode (str): padding mode for outside grid values + 'zeros' | 'border' | 'reflection'. Default: 'zeros'. + + Returns: + torch.Tensor: the warped tensor. + + Shape: + - Output: :math:`(B, C, H, W)` + + .. note:: + See a working example `here `__. + """ + if not torch.is_tensor(src): + raise TypeError("Input src type is not a torch.Tensor. Got {}".format(type(src))) + + if not torch.is_tensor(M): + raise TypeError("Input M type is not a torch.Tensor. Got {}".format(type(M))) + + if not len(src.shape) == 4: + raise ValueError("Input src must be a BxCxHxW tensor. Got {}".format(src.shape)) + + if not (len(M.shape) == 3 or M.shape[-2:] == (2, 3)): + raise ValueError("Input M must be a Bx2x3 tensor. Got {}".format(src.shape)) + + # we generate a 3x3 transformation matrix from 2x3 affine + M_3x3: torch.Tensor = F.pad(M, [0, 0, 0, 1, 0, 0], mode="constant", value=0) + M_3x3[:, 2, 2] += 1.0 + + # launches the warper + h, w = src.shape[-2:] + return transform_warp_impl(src, centers, M_3x3, (h, w), dsize, flags, padding_mode) diff --git a/forge/test/models/pytorch/multimodal/trajectron/trajectron/model/dataset/preprocessing.py b/forge/test/models/pytorch/multimodal/trajectron/trajectron/model/dataset/preprocessing.py new file mode 100644 index 000000000..21d42d9b6 --- /dev/null +++ b/forge/test/models/pytorch/multimodal/trajectron/trajectron/model/dataset/preprocessing.py @@ -0,0 +1,261 @@ +# SPDX-FileCopyrightText: (c) 2024 Tenstorrent AI ULC +# +# SPDX-License-Identifier: Apache-2.0 +import torch +import numpy as np +import collections.abc +from torch.utils.data._utils.collate import default_collate +import dill + +container_abcs = collections.abc + + +def restore(data): + """ + In case we dilled some structures to share between multiple process this function will restore them. + If the data input are not bytes we assume it was not dilled in the first place + + :param data: Possibly dilled data structure + :return: Un-dilled data structure + """ + if type(data) is bytes: + return dill.loads(data) + return data + + +def collate(batch): + if len(batch) == 0: + return batch + elem = batch[0] + if elem is None: + return None + elif isinstance(elem, container_abcs.Sequence): + if len(elem) == 4: # We assume those are the maps, map points, headings and patch_size + scene_map, scene_pts, heading_angle, patch_size = zip(*batch) + if heading_angle[0] is None: + heading_angle = None + else: + heading_angle = torch.Tensor(heading_angle) + map = scene_map[0].get_cropped_maps_from_scene_map_batch( + scene_map, scene_pts=torch.Tensor(scene_pts), patch_size=patch_size[0], rotation=heading_angle + ) + return map + transposed = zip(*batch) + return [collate(samples) for samples in transposed] + elif isinstance(elem, container_abcs.Mapping): + # We have to dill the neighbors structures. Otherwise each tensor is put into + # shared memory separately -> slow, file pointer overhead + # we only do this in multiprocessing + neighbor_dict = {key: [d[key] for d in batch] for key in elem} + return dill.dumps(neighbor_dict) if torch.utils.data.get_worker_info() else neighbor_dict + return default_collate(batch) + + +def get_relative_robot_traj(env, state, node_traj, robot_traj, node_type, robot_type): + # TODO: We will have to make this more generic if robot_type != node_type + # Make Robot State relative to node + _, std = env.get_standardize_params(state[robot_type], node_type=robot_type) + std[0:2] = env.attention_radius[(node_type, robot_type)] + robot_traj_st = env.standardize(robot_traj, state[robot_type], node_type=robot_type, mean=node_traj, std=std) + robot_traj_st_t = torch.tensor(robot_traj_st, dtype=torch.float) + + return robot_traj_st_t + + +def get_node_timestep_data( + env, scene, t, node, state, pred_state, edge_types, max_ht, max_ft, hyperparams, scene_graph=None +): + """ + Pre-processes the data for a single batch element: node state over time for a specific time in a specific scene + as well as the neighbour data for it. + + :param env: Environment + :param scene: Scene + :param t: Timestep in scene + :param node: Node + :param state: Specification of the node state + :param pred_state: Specification of the prediction state + :param edge_types: List of all Edge Types for which neighbours are pre-processed + :param max_ht: Maximum history timesteps + :param max_ft: Maximum future timesteps (prediction horizon) + :param hyperparams: Model hyperparameters + :param scene_graph: If scene graph was already computed for this scene and time you can pass it here + :return: Batch Element + """ + + # Node + timestep_range_x = np.array([t - max_ht, t]) + timestep_range_y = np.array([t + 1, t + max_ft]) + + x = node.get(timestep_range_x, state[node.type]) + y = node.get(timestep_range_y, pred_state[node.type]) + first_history_index = (max_ht - node.history_points_at(t)).clip(0) + + _, std = env.get_standardize_params(state[node.type], node.type) + std[0:2] = env.attention_radius[(node.type, node.type)] + rel_state = np.zeros_like(x[0]) + rel_state[0:2] = np.array(x)[-1, 0:2] + x_st = env.standardize(x, state[node.type], node.type, mean=rel_state, std=std) + if list(pred_state[node.type].keys())[0] == "position": # If we predict position we do it relative to current pos + y_st = env.standardize(y, pred_state[node.type], node.type, mean=rel_state[0:2]) + else: + y_st = env.standardize(y, pred_state[node.type], node.type) + + x_t = torch.tensor(x, dtype=torch.float) + y_t = torch.tensor(y, dtype=torch.float) + x_st_t = torch.tensor(x_st, dtype=torch.float) + y_st_t = torch.tensor(y_st, dtype=torch.float) + + # Neighbors + neighbors_data_st = None + neighbors_edge_value = None + if hyperparams["edge_encoding"]: + # Scene Graph + scene_graph = ( + scene.get_scene_graph( + t, env.attention_radius, hyperparams["edge_addition_filter"], hyperparams["edge_removal_filter"] + ) + if scene_graph is None + else scene_graph + ) + + neighbors_data_st = dict() + neighbors_edge_value = dict() + for edge_type in edge_types: + neighbors_data_st[edge_type] = list() + # We get all nodes which are connected to the current node for the current timestep + connected_nodes = scene_graph.get_neighbors(node, edge_type[1]) + + if hyperparams["dynamic_edges"] == "yes": + # We get the edge masks for the current node at the current timestep + edge_masks = torch.tensor(scene_graph.get_edge_scaling(node), dtype=torch.float) + neighbors_edge_value[edge_type] = edge_masks + + for connected_node in connected_nodes: + neighbor_state_np = connected_node.get( + np.array([t - max_ht, t]), state[connected_node.type], padding=0.0 + ) + + # Make State relative to node where neighbor and node have same state + _, std = env.get_standardize_params(state[connected_node.type], node_type=connected_node.type) + std[0:2] = env.attention_radius[edge_type] + equal_dims = np.min((neighbor_state_np.shape[-1], x.shape[-1])) + rel_state = np.zeros_like(neighbor_state_np) + rel_state[:, ..., :equal_dims] = x[-1, ..., :equal_dims] + neighbor_state_np_st = env.standardize( + neighbor_state_np, + state[connected_node.type], + node_type=connected_node.type, + mean=rel_state, + std=std, + ) + + neighbor_state = torch.tensor(neighbor_state_np_st, dtype=torch.float) + neighbors_data_st[edge_type].append(neighbor_state) + + # Robot + robot_traj_st_t = None + if hyperparams["incl_robot_node"]: + timestep_range_r = np.array([t, t + max_ft]) + if scene.non_aug_scene is not None: + robot = scene.get_node_by_id(scene.non_aug_scene.robot.id) + else: + robot = scene.robot + robot_type = robot.type + robot_traj = robot.get(timestep_range_r, state[robot_type], padding=0.0) + node_state = np.zeros_like(robot_traj[0]) + node_state[: x.shape[1]] = x[-1] + robot_traj_st_t = get_relative_robot_traj(env, state, node_state, robot_traj, node.type, robot_type) + + # Map + map_tuple = None + if hyperparams["use_map_encoding"]: + if node.type in hyperparams["map_encoder"]: + if node.non_aug_node is not None: + x = node.non_aug_node.get(np.array([t]), state[node.type]) + me_hyp = hyperparams["map_encoder"][node.type] + if "heading_state_index" in me_hyp: + heading_state_index = me_hyp["heading_state_index"] + # We have to rotate the map in the opposit direction of the agent to match them + if type(heading_state_index) is list: # infer from velocity or heading vector + heading_angle = ( + -np.arctan2(x[-1, heading_state_index[1]], x[-1, heading_state_index[0]]) * 180 / np.pi + ) + else: + heading_angle = -x[-1, heading_state_index] * 180 / np.pi + else: + heading_angle = None + + scene_map = scene.map[node.type] + map_point = x[-1, :2] + + patch_size = hyperparams["map_encoder"][node.type]["patch_size"] + map_tuple = (scene_map, map_point, heading_angle, patch_size) + + return ( + first_history_index, + x_t, + y_t, + x_st_t, + y_st_t, + neighbors_data_st, + neighbors_edge_value, + robot_traj_st_t, + map_tuple, + ) + + +def get_timesteps_data( + env, scene, t, node_type, state, pred_state, edge_types, min_ht, max_ht, min_ft, max_ft, hyperparams +): + """ + Puts together the inputs for ALL nodes in a given scene and timestep in it. + + :param env: Environment + :param scene: Scene + :param t: Timestep in scene + :param node_type: Node Type of nodes for which the data shall be pre-processed + :param state: Specification of the node state + :param pred_state: Specification of the prediction state + :param edge_types: List of all Edge Types for which neighbors are pre-processed + :param max_ht: Maximum history timesteps + :param max_ft: Maximum future timesteps (prediction horizon) + :param hyperparams: Model hyperparameters + :return: + """ + nodes_per_ts = scene.present_nodes( + t, + type=node_type, + min_history_timesteps=min_ht, + min_future_timesteps=max_ft, + return_robot=not hyperparams["incl_robot_node"], + ) + batch = list() + nodes = list() + out_timesteps = list() + for timestep in nodes_per_ts.keys(): + scene_graph = scene.get_scene_graph( + timestep, env.attention_radius, hyperparams["edge_addition_filter"], hyperparams["edge_removal_filter"] + ) + present_nodes = nodes_per_ts[timestep] + for node in present_nodes: + nodes.append(node) + out_timesteps.append(timestep) + batch.append( + get_node_timestep_data( + env, + scene, + timestep, + node, + state, + pred_state, + edge_types, + max_ht, + max_ft, + hyperparams, + scene_graph=scene_graph, + ) + ) + if len(out_timesteps) == 0: + return None + return collate(batch), nodes, out_timesteps diff --git a/forge/test/models/pytorch/multimodal/trajectron/trajectron/model/dynamics/__init__.py b/forge/test/models/pytorch/multimodal/trajectron/trajectron/model/dynamics/__init__.py new file mode 100644 index 000000000..968fc5653 --- /dev/null +++ b/forge/test/models/pytorch/multimodal/trajectron/trajectron/model/dynamics/__init__.py @@ -0,0 +1,7 @@ +# SPDX-FileCopyrightText: (c) 2024 Tenstorrent AI ULC +# +# SPDX-License-Identifier: Apache-2.0 +from model.dynamics.dynamic import Dynamic +from model.dynamics.single_integrator import SingleIntegrator +from model.dynamics.unicycle import Unicycle +from model.dynamics.linear import Linear diff --git a/forge/test/models/pytorch/multimodal/trajectron/trajectron/model/dynamics/dynamic.py b/forge/test/models/pytorch/multimodal/trajectron/trajectron/model/dynamics/dynamic.py new file mode 100644 index 000000000..4fd3b6b0f --- /dev/null +++ b/forge/test/models/pytorch/multimodal/trajectron/trajectron/model/dynamics/dynamic.py @@ -0,0 +1,33 @@ +# SPDX-FileCopyrightText: (c) 2024 Tenstorrent AI ULC +# +# SPDX-License-Identifier: Apache-2.0 + + +class Dynamic(object): + def __init__(self, dt, dyn_limits, device, model_registrar, xz_size, node_type): + self.dt = dt + self.device = device + self.dyn_limits = dyn_limits + self.initial_conditions = None + self.model_registrar = model_registrar + self.node_type = node_type + self.init_constants() + self.create_graph(xz_size) + + def set_initial_condition(self, init_con): + self.initial_conditions = init_con + + def init_constants(self): + pass + + def create_graph(self, xz_size): + pass + + def integrate_samples(self, s, x): + raise NotImplementedError + + def integrate_distribution(self, dist, x): + raise NotImplementedError + + def create_graph(self, xz_size): + pass diff --git a/forge/test/models/pytorch/multimodal/trajectron/trajectron/model/dynamics/linear.py b/forge/test/models/pytorch/multimodal/trajectron/trajectron/model/dynamics/linear.py new file mode 100644 index 000000000..228df8008 --- /dev/null +++ b/forge/test/models/pytorch/multimodal/trajectron/trajectron/model/dynamics/linear.py @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: (c) 2024 Tenstorrent AI ULC +# +# SPDX-License-Identifier: Apache-2.0 +from ..dynamics import Dynamic + + +class Linear(Dynamic): + def init_constants(self): + pass + + def integrate_samples(self, v, x): + return v + + def integrate_distribution(self, v_dist, x): + return v_dist diff --git a/forge/test/models/pytorch/multimodal/trajectron/trajectron/model/dynamics/single_integrator.py b/forge/test/models/pytorch/multimodal/trajectron/trajectron/model/dynamics/single_integrator.py new file mode 100644 index 000000000..cb2cfeb2e --- /dev/null +++ b/forge/test/models/pytorch/multimodal/trajectron/trajectron/model/dynamics/single_integrator.py @@ -0,0 +1,67 @@ +# SPDX-FileCopyrightText: (c) 2024 Tenstorrent AI ULC +# +# SPDX-License-Identifier: Apache-2.0 +import torch +from model.dynamics import Dynamic +from utils import block_diag +from model.components import GMM2D + + +class SingleIntegrator(Dynamic): + def init_constants(self): + self.F = torch.eye(4, device=self.device, dtype=torch.float32) + self.F[0:2, 2:] = torch.eye(2, device=self.device, dtype=torch.float32) * self.dt + self.F_t = self.F.transpose(-2, -1) + + def integrate_samples(self, v, x=None): + """ + Integrates deterministic samples of velocity. + + :param v: Velocity samples + :param x: Not used for SI. + :return: Position samples + """ + p_0 = self.initial_conditions["pos"].unsqueeze(1) + return torch.cumsum(v, dim=2) * self.dt + p_0 + + def integrate_distribution(self, v_dist, x=None): + r""" + Integrates the GMM velocity distribution to a distribution over position. + The Kalman Equations are used. + + .. math:: \mu_{t+1} =\textbf{F} \mu_{t} + + .. math:: \mathbf{\Sigma}_{t+1}={\textbf {F}} \mathbf{\Sigma}_{t} {\textbf {F}}^{T} + + .. math:: + \textbf{F} = \left[ + \begin{array}{cccc} + \sigma_x^2 & \rho_p \sigma_x \sigma_y & 0 & 0 \\ + \rho_p \sigma_x \sigma_y & \sigma_y^2 & 0 & 0 \\ + 0 & 0 & \sigma_{v_x}^2 & \rho_v \sigma_{v_x} \sigma_{v_y} \\ + 0 & 0 & \rho_v \sigma_{v_x} \sigma_{v_y} & \sigma_{v_y}^2 \\ + \end{array} + \right]_{t} + + :param v_dist: Joint GMM Distribution over velocity in x and y direction. + :param x: Not used for SI. + :return: Joint GMM Distribution over position in x and y direction. + """ + p_0 = self.initial_conditions["pos"].unsqueeze(1) + ph = v_dist.mus.shape[-3] + sample_batch_dim = list(v_dist.mus.shape[0:2]) + pos_dist_sigma_matrix_list = [] + + pos_mus = p_0[:, None] + torch.cumsum(v_dist.mus, dim=2) * self.dt + + vel_dist_sigma_matrix = v_dist.get_covariance_matrix() + pos_dist_sigma_matrix_t = torch.zeros(sample_batch_dim + [v_dist.components, 2, 2], device=self.device) + + for t in range(ph): + vel_sigma_matrix_t = vel_dist_sigma_matrix[:, :, t] + full_sigma_matrix_t = block_diag([pos_dist_sigma_matrix_t, vel_sigma_matrix_t]) + pos_dist_sigma_matrix_t = self.F[..., :2, :].matmul(full_sigma_matrix_t.matmul(self.F_t)[..., :2]) + pos_dist_sigma_matrix_list.append(pos_dist_sigma_matrix_t) + + pos_dist_sigma_matrix = torch.stack(pos_dist_sigma_matrix_list, dim=2) + return GMM2D.from_log_pis_mus_cov_mats(v_dist.log_pis, pos_mus, pos_dist_sigma_matrix) diff --git a/forge/test/models/pytorch/multimodal/trajectron/trajectron/model/dynamics/unicycle.py b/forge/test/models/pytorch/multimodal/trajectron/trajectron/model/dynamics/unicycle.py new file mode 100644 index 000000000..b46820063 --- /dev/null +++ b/forge/test/models/pytorch/multimodal/trajectron/trajectron/model/dynamics/unicycle.py @@ -0,0 +1,239 @@ +# SPDX-FileCopyrightText: (c) 2024 Tenstorrent AI ULC +# +# SPDX-License-Identifier: Apache-2.0 +import torch +import torch.nn as nn +from model.dynamics import Dynamic +from utils import block_diag +from model.components import GMM2D + + +class Unicycle(Dynamic): + def init_constants(self): + self.F_s = torch.eye(4, device=self.device, dtype=torch.float32) + self.F_s[0:2, 2:] = torch.eye(2, device=self.device, dtype=torch.float32) * self.dt + self.F_s_t = self.F_s.transpose(-2, -1) + + def create_graph(self, xz_size): + model_if_absent = nn.Linear(xz_size + 1, 1) + self.p0_model = self.model_registrar.get_model(f"{self.node_type}/unicycle_initializer", model_if_absent) + + def dynamic(self, x, u): + r""" + TODO: Boris: Add docstring + :param x: + :param u: + :return: + """ + x_p = x[0] + y_p = x[1] + phi = x[2] + v = x[3] + dphi = u[0] + a = u[1] + + mask = torch.abs(dphi) <= 1e-2 + dphi = ~mask * dphi + (mask) * 1 + + phi_p_omega_dt = phi + dphi * self.dt + dsin_domega = (torch.sin(phi_p_omega_dt) - torch.sin(phi)) / dphi + dcos_domega = (torch.cos(phi_p_omega_dt) - torch.cos(phi)) / dphi + + d1 = torch.stack( + [ + (x_p + (a / dphi) * dcos_domega + v * dsin_domega + (a / dphi) * torch.sin(phi_p_omega_dt) * self.dt), + (y_p - v * dcos_domega + (a / dphi) * dsin_domega - (a / dphi) * torch.cos(phi_p_omega_dt) * self.dt), + phi + dphi * self.dt, + v + a * self.dt, + ], + dim=0, + ) + d2 = torch.stack( + [ + x_p + v * torch.cos(phi) * self.dt + (a / 2) * torch.cos(phi) * self.dt**2, + y_p + v * torch.sin(phi) * self.dt + (a / 2) * torch.sin(phi) * self.dt**2, + phi * torch.ones_like(a), + v + a * self.dt, + ], + dim=0, + ) + return torch.where(~mask, d1, d2) + + def integrate_samples(self, control_samples, x=None): + r""" + TODO: Boris: Add docstring + :param x: + :param u: + :return: + """ + ph = control_samples.shape[-2] + p_0 = self.initial_conditions["pos"].unsqueeze(1) + v_0 = self.initial_conditions["vel"].unsqueeze(1) + + # In case the input is batched because of the robot in online use we repeat this to match the batch size of x. + if p_0.size()[0] != x.size()[0]: + p_0 = p_0.repeat(x.size()[0], 1, 1) + v_0 = v_0.repeat(x.size()[0], 1, 1) + + phi_0 = torch.atan2(v_0[..., 1], v_0[..., 0]) + + phi_0 = phi_0 + torch.tanh(self.p0_model(torch.cat((x, phi_0), dim=-1))) + + u = torch.stack([control_samples[..., 0], control_samples[..., 1]], dim=0) + x = torch.stack([p_0[..., 0], p_0[..., 1], phi_0, torch.norm(v_0, dim=-1)], dim=0).squeeze(dim=-1) + + mus_list = [] + for t in range(ph): + x = self.dynamic(x, u[..., t]) + mus_list.append(torch.stack((x[0], x[1]), dim=-1)) + + pos_mus = torch.stack(mus_list, dim=2) + return pos_mus + + def compute_control_jacobian(self, sample_batch_dim, components, x, u): + r""" + TODO: Boris: Add docstring + :param x: + :param u: + :return: + """ + F = torch.zeros(sample_batch_dim + [components, 4, 2], device=self.device, dtype=torch.float32) + + phi = x[2] + v = x[3] + dphi = u[0] + a = u[1] + + mask = torch.abs(dphi) <= 1e-2 + dphi = ~mask * dphi + (mask) * 1 + + phi_p_omega_dt = phi + dphi * self.dt + dsin_domega = (torch.sin(phi_p_omega_dt) - torch.sin(phi)) / dphi + dcos_domega = (torch.cos(phi_p_omega_dt) - torch.cos(phi)) / dphi + + F[..., 0, 0] = ( + (v / dphi) * torch.cos(phi_p_omega_dt) * self.dt + - (v / dphi) * dsin_domega + - (2 * a / dphi**2) * torch.sin(phi_p_omega_dt) * self.dt + - (2 * a / dphi**2) * dcos_domega + + (a / dphi) * torch.cos(phi_p_omega_dt) * self.dt**2 + ) + F[..., 0, 1] = (1 / dphi) * dcos_domega + (1 / dphi) * torch.sin(phi_p_omega_dt) * self.dt + + F[..., 1, 0] = ( + (v / dphi) * dcos_domega + - (2 * a / dphi**2) * dsin_domega + + (2 * a / dphi**2) * torch.cos(phi_p_omega_dt) * self.dt + + (v / dphi) * torch.sin(phi_p_omega_dt) * self.dt + + (a / dphi) * torch.sin(phi_p_omega_dt) * self.dt**2 + ) + F[..., 1, 1] = (1 / dphi) * dsin_domega - (1 / dphi) * torch.cos(phi_p_omega_dt) * self.dt + + F[..., 2, 0] = self.dt + + F[..., 3, 1] = self.dt + + F_sm = torch.zeros(sample_batch_dim + [components, 4, 2], device=self.device, dtype=torch.float32) + + F_sm[..., 0, 1] = (torch.cos(phi) * self.dt**2) / 2 + + F_sm[..., 1, 1] = (torch.sin(phi) * self.dt**2) / 2 + + F_sm[..., 3, 1] = self.dt + + return torch.where(~mask.unsqueeze(-1).unsqueeze(-1), F, F_sm) + + def compute_jacobian(self, sample_batch_dim, components, x, u): + r""" + TODO: Boris: Add docstring + :param x: + :param u: + :return: + """ + one = torch.tensor(1) + F = torch.zeros(sample_batch_dim + [components, 4, 4], device=self.device, dtype=torch.float32) + + phi = x[2] + v = x[3] + dphi = u[0] + a = u[1] + + mask = torch.abs(dphi) <= 1e-2 + dphi = ~mask * dphi + (mask) * 1 + + phi_p_omega_dt = phi + dphi * self.dt + dsin_domega = (torch.sin(phi_p_omega_dt) - torch.sin(phi)) / dphi + dcos_domega = (torch.cos(phi_p_omega_dt) - torch.cos(phi)) / dphi + + F[..., 0, 0] = one + F[..., 1, 1] = one + F[..., 2, 2] = one + F[..., 3, 3] = one + + F[..., 0, 2] = v * dcos_domega - (a / dphi) * dsin_domega + (a / dphi) * torch.cos(phi_p_omega_dt) * self.dt + F[..., 0, 3] = dsin_domega + + F[..., 1, 2] = v * dsin_domega + (a / dphi) * dcos_domega + (a / dphi) * torch.sin(phi_p_omega_dt) * self.dt + F[..., 1, 3] = -dcos_domega + + F_sm = torch.zeros(sample_batch_dim + [components, 4, 4], device=self.device, dtype=torch.float32) + + F_sm[..., 0, 0] = one + F_sm[..., 1, 1] = one + F_sm[..., 2, 2] = one + F_sm[..., 3, 3] = one + + F_sm[..., 0, 2] = -v * torch.sin(phi) * self.dt - (a * torch.sin(phi) * self.dt**2) / 2 + F_sm[..., 0, 3] = torch.cos(phi) * self.dt + + F_sm[..., 1, 2] = v * torch.cos(phi) * self.dt + (a * torch.cos(phi) * self.dt**2) / 2 + F_sm[..., 1, 3] = torch.sin(phi) * self.dt + + return torch.where(~mask.unsqueeze(-1).unsqueeze(-1), F, F_sm) + + def integrate_distribution(self, control_dist_dphi_a, x): + r""" + TODO: Boris: Add docstring + :param x: + :param u: + :return: + """ + sample_batch_dim = list(control_dist_dphi_a.mus.shape[0:2]) + ph = control_dist_dphi_a.mus.shape[-3] + p_0 = self.initial_conditions["pos"].unsqueeze(1) + v_0 = self.initial_conditions["vel"].unsqueeze(1) + + # In case the input is batched because of the robot in online use we repeat this to match the batch size of x. + if p_0.size()[0] != x.size()[0]: + p_0 = p_0.repeat(x.size()[0], 1, 1) + v_0 = v_0.repeat(x.size()[0], 1, 1) + + phi_0 = torch.atan2(v_0[..., 1], v_0[..., 0]) + + phi_0 = phi_0 + torch.tanh(self.p0_model(torch.cat((x, phi_0), dim=-1))) + + dist_sigma_matrix = control_dist_dphi_a.get_covariance_matrix() + pos_dist_sigma_matrix_t = torch.zeros( + sample_batch_dim + [control_dist_dphi_a.components, 4, 4], device=self.device + ) + + u = torch.stack([control_dist_dphi_a.mus[..., 0], control_dist_dphi_a.mus[..., 1]], dim=0) + x = torch.stack([p_0[..., 0], p_0[..., 1], phi_0, torch.norm(v_0, dim=-1)], dim=0) + + pos_dist_sigma_matrix_list = [] + mus_list = [] + for t in range(ph): + F_t = self.compute_jacobian(sample_batch_dim, control_dist_dphi_a.components, x, u[:, :, :, t]) + G_t = self.compute_control_jacobian(sample_batch_dim, control_dist_dphi_a.components, x, u[:, :, :, t]) + dist_sigma_matrix_t = dist_sigma_matrix[:, :, t] + pos_dist_sigma_matrix_t = F_t.matmul(pos_dist_sigma_matrix_t.matmul(F_t.transpose(-2, -1))) + G_t.matmul( + dist_sigma_matrix_t.matmul(G_t.transpose(-2, -1)) + ) + pos_dist_sigma_matrix_list.append(pos_dist_sigma_matrix_t[..., :2, :2]) + + x = self.dynamic(x, u[:, :, :, t]) + mus_list.append(torch.stack((x[0], x[1]), dim=-1)) + + pos_dist_sigma_matrix = torch.stack(pos_dist_sigma_matrix_list, dim=2) + pos_mus = torch.stack(mus_list, dim=2) + return GMM2D.from_log_pis_mus_cov_mats(control_dist_dphi_a.log_pis, pos_mus, pos_dist_sigma_matrix) diff --git a/forge/test/models/pytorch/multimodal/trajectron/trajectron/model/mgcvae.py b/forge/test/models/pytorch/multimodal/trajectron/trajectron/model/mgcvae.py new file mode 100644 index 000000000..c05e86229 --- /dev/null +++ b/forge/test/models/pytorch/multimodal/trajectron/trajectron/model/mgcvae.py @@ -0,0 +1,1240 @@ +# SPDX-FileCopyrightText: (c) 2024 Tenstorrent AI ULC +# +# SPDX-License-Identifier: Apache-2.0 +import warnings +import torch.nn as nn +import torch.nn.functional as F +import torch.optim as optim +from model.components import * +from model.model_utils import * +import model.dynamics as dynamic_module +from environment.scene_graph import DirectedEdge + + +class MultimodalGenerativeCVAE(torch.nn.Module): + def __init__(self, env, node_type, model_registrar, hyperparams, device, edge_types, log_writer=None): + super().__init__() + self.hyperparams = hyperparams + self.env = env + self.node_type = node_type + self.model_registrar = model_registrar + self.log_writer = log_writer + self.device = device + self.edge_types = [edge_type for edge_type in edge_types if edge_type[0] is node_type] + self.curr_iter = 0 + + self.node_modules = dict() + self.node_modules = torch.nn.ModuleDict() + + self.min_hl = self.hyperparams["minimum_history_length"] + self.max_hl = self.hyperparams["maximum_history_length"] + self.ph = self.hyperparams["prediction_horizon"] + self.state = self.hyperparams["state"] + self.pred_state = self.hyperparams["pred_state"][node_type] + self.state_length = int(np.sum([len(entity_dims) for entity_dims in self.state[node_type].values()])) + if self.hyperparams["incl_robot_node"]: + self.robot_state_length = int( + np.sum([len(entity_dims) for entity_dims in self.state[env.robot_type].values()]) + ) + self.pred_state_length = int(np.sum([len(entity_dims) for entity_dims in self.pred_state.values()])) + + edge_types_str = [DirectedEdge.get_str_from_types(*edge_type) for edge_type in self.edge_types] + self.create_graphical_model(edge_types_str) + + dynamic_class = getattr(dynamic_module, hyperparams["dynamic"][self.node_type]["name"]) + dyn_limits = hyperparams["dynamic"][self.node_type]["limits"] + self.dynamic = dynamic_class( + self.env.scenes[0].dt, dyn_limits, device, self.model_registrar, self.x_size, self.node_type + ) + + def eval(self): + super().eval() + for key in self.node_modules.keys(): + self.node_modules[key].eval() + + def set_curr_iter(self, curr_iter): + self.curr_iter = curr_iter + + def add_submodule(self, name, model_if_absent): + self.node_modules[name] = self.model_registrar.get_model(name, model_if_absent) + + def clear_submodules(self): + self.node_modules.clear() + + def create_node_models(self): + ############################ + # Node History Encoder # + ############################ + self.add_submodule( + self.node_type + "/node_history_encoder", + model_if_absent=nn.LSTM( + input_size=self.state_length, hidden_size=self.hyperparams["enc_rnn_dim_history"], batch_first=True + ), + ) + + ########################### + # Node Future Encoder # + ########################### + # We'll create this here, but then later check if in training mode. + # Based on that, we'll factor this into the computation graph (or not). + self.add_submodule( + self.node_type + "/node_future_encoder", + model_if_absent=nn.LSTM( + input_size=self.pred_state_length, + hidden_size=self.hyperparams["enc_rnn_dim_future"], + bidirectional=True, + batch_first=True, + ), + ) + # These are related to how you initialize states for the node future encoder. + self.add_submodule( + self.node_type + "/node_future_encoder/initial_h", + model_if_absent=nn.Linear(self.state_length, self.hyperparams["enc_rnn_dim_future"]), + ) + self.add_submodule( + self.node_type + "/node_future_encoder/initial_c", + model_if_absent=nn.Linear(self.state_length, self.hyperparams["enc_rnn_dim_future"]), + ) + + ############################ + # Robot Future Encoder # + ############################ + # We'll create this here, but then later check if we're next to the robot. + # Based on that, we'll factor this into the computation graph (or not). + if self.hyperparams["incl_robot_node"]: + self.add_submodule( + "robot_future_encoder", + model_if_absent=nn.LSTM( + input_size=self.robot_state_length, + hidden_size=self.hyperparams["enc_rnn_dim_future"], + bidirectional=True, + batch_first=True, + ), + ) + # These are related to how you initialize states for the robot future encoder. + self.add_submodule( + "robot_future_encoder/initial_h", + model_if_absent=nn.Linear(self.robot_state_length, self.hyperparams["enc_rnn_dim_future"]), + ) + self.add_submodule( + "robot_future_encoder/initial_c", + model_if_absent=nn.Linear(self.robot_state_length, self.hyperparams["enc_rnn_dim_future"]), + ) + + if self.hyperparams["edge_encoding"]: + ############################## + # Edge Influence Encoder # + ############################## + # NOTE: The edge influence encoding happens during calls + # to forward or incremental_forward, so we don't create + # a model for it here for the max and sum variants. + if self.hyperparams["edge_influence_combine_method"] == "bi-rnn": + self.add_submodule( + self.node_type + "/edge_influence_encoder", + model_if_absent=nn.LSTM( + input_size=self.hyperparams["enc_rnn_dim_edge"], + hidden_size=self.hyperparams["enc_rnn_dim_edge_influence"], + bidirectional=True, + batch_first=True, + ), + ) + + # Four times because we're trying to mimic a bi-directional + # LSTM's output (which, here, is c and h from both ends). + self.eie_output_dims = 4 * self.hyperparams["enc_rnn_dim_edge_influence"] + + elif self.hyperparams["edge_influence_combine_method"] == "attention": + # Chose additive attention because of https://arxiv.org/pdf/1703.03906.pdf + # We calculate an attention context vector using the encoded edges as the "encoder" + # (that we attend _over_) + # and the node history encoder representation as the "decoder state" (that we attend _on_). + self.add_submodule( + self.node_type + "/edge_influence_encoder", + model_if_absent=AdditiveAttention( + encoder_hidden_state_dim=self.hyperparams["enc_rnn_dim_edge_influence"], + decoder_hidden_state_dim=self.hyperparams["enc_rnn_dim_history"], + ), + ) + + self.eie_output_dims = self.hyperparams["enc_rnn_dim_edge_influence"] + + ################### + # Map Encoder # + ################### + if self.hyperparams["use_map_encoding"]: + if self.node_type in self.hyperparams["map_encoder"]: + me_params = self.hyperparams["map_encoder"][self.node_type] + self.add_submodule( + self.node_type + "/map_encoder", + model_if_absent=CNNMapEncoder( + me_params["map_channels"], + me_params["hidden_channels"], + me_params["output_size"], + me_params["masks"], + me_params["strides"], + me_params["patch_size"], + ), + ) + + ################################ + # Discrete Latent Variable # + ################################ + self.latent = DiscreteLatent(self.hyperparams, self.device) + + ###################################################################### + # Various Fully-Connected Layers from Encoder to Latent Variable # + ###################################################################### + # Node History Encoder + x_size = self.hyperparams["enc_rnn_dim_history"] + if self.hyperparams["edge_encoding"]: + # Edge Encoder + x_size += self.eie_output_dims + if self.hyperparams["incl_robot_node"]: + # Future Conditional Encoder + x_size += 4 * self.hyperparams["enc_rnn_dim_future"] + if self.hyperparams["use_map_encoding"] and self.node_type in self.hyperparams["map_encoder"]: + # Map Encoder + x_size += self.hyperparams["map_encoder"][self.node_type]["output_size"] + + z_size = self.hyperparams["N"] * self.hyperparams["K"] + + if self.hyperparams["p_z_x_MLP_dims"] is not None: + self.add_submodule( + self.node_type + "/p_z_x", model_if_absent=nn.Linear(x_size, self.hyperparams["p_z_x_MLP_dims"]) + ) + hx_size = self.hyperparams["p_z_x_MLP_dims"] + else: + hx_size = x_size + + self.add_submodule(self.node_type + "/hx_to_z", model_if_absent=nn.Linear(hx_size, self.latent.z_dim)) + + if self.hyperparams["q_z_xy_MLP_dims"] is not None: + self.add_submodule( + self.node_type + "/q_z_xy", + # Node Future Encoder + model_if_absent=nn.Linear( + x_size + 4 * self.hyperparams["enc_rnn_dim_future"], self.hyperparams["q_z_xy_MLP_dims"] + ), + ) + hxy_size = self.hyperparams["q_z_xy_MLP_dims"] + else: + # Node Future Encoder + hxy_size = x_size + 4 * self.hyperparams["enc_rnn_dim_future"] + + self.add_submodule(self.node_type + "/hxy_to_z", model_if_absent=nn.Linear(hxy_size, self.latent.z_dim)) + + #################### + # Decoder LSTM # + #################### + if self.hyperparams["incl_robot_node"]: + decoder_input_dims = self.pred_state_length + self.robot_state_length + z_size + x_size + else: + decoder_input_dims = self.pred_state_length + z_size + x_size + + self.add_submodule( + self.node_type + "/decoder/state_action", + model_if_absent=nn.Sequential(nn.Linear(self.state_length, self.pred_state_length)), + ) + + self.add_submodule( + self.node_type + "/decoder/rnn_cell", + model_if_absent=nn.GRUCell(decoder_input_dims, self.hyperparams["dec_rnn_dim"]), + ) + self.add_submodule( + self.node_type + "/decoder/initial_h", + model_if_absent=nn.Linear(z_size + x_size, self.hyperparams["dec_rnn_dim"]), + ) + + ################### + # Decoder GMM # + ################### + self.add_submodule( + self.node_type + "/decoder/proj_to_GMM_log_pis", + model_if_absent=nn.Linear(self.hyperparams["dec_rnn_dim"], self.hyperparams["GMM_components"]), + ) + self.add_submodule( + self.node_type + "/decoder/proj_to_GMM_mus", + model_if_absent=nn.Linear( + self.hyperparams["dec_rnn_dim"], self.hyperparams["GMM_components"] * self.pred_state_length + ), + ) + self.add_submodule( + self.node_type + "/decoder/proj_to_GMM_log_sigmas", + model_if_absent=nn.Linear( + self.hyperparams["dec_rnn_dim"], self.hyperparams["GMM_components"] * self.pred_state_length + ), + ) + self.add_submodule( + self.node_type + "/decoder/proj_to_GMM_corrs", + model_if_absent=nn.Linear(self.hyperparams["dec_rnn_dim"], self.hyperparams["GMM_components"]), + ) + + self.x_size = x_size + self.z_size = z_size + + def create_edge_models(self, edge_types): + for edge_type in edge_types: + neighbor_state_length = int( + np.sum([len(entity_dims) for entity_dims in self.state[edge_type.split("->")[1]].values()]) + ) + if self.hyperparams["edge_state_combine_method"] == "pointnet": + self.add_submodule( + edge_type + "/pointnet_encoder", + model_if_absent=nn.Sequential( + nn.Linear(self.state_length, 2 * self.state_length), + nn.ReLU(), + nn.Linear(2 * self.state_length, 2 * self.state_length), + nn.ReLU(), + ), + ) + + edge_encoder_input_size = 2 * self.state_length + self.state_length + + elif self.hyperparams["edge_state_combine_method"] == "attention": + self.add_submodule( + self.node_type + "/edge_attention_combine", + model_if_absent=TemporallyBatchedAdditiveAttention( + encoder_hidden_state_dim=self.state_length, decoder_hidden_state_dim=self.state_length + ), + ) + edge_encoder_input_size = self.state_length + neighbor_state_length + + else: + edge_encoder_input_size = self.state_length + neighbor_state_length + + self.add_submodule( + edge_type + "/edge_encoder", + model_if_absent=nn.LSTM( + input_size=edge_encoder_input_size, + hidden_size=self.hyperparams["enc_rnn_dim_edge"], + batch_first=True, + ), + ) + + def create_graphical_model(self, edge_types): + """ + Creates or queries all trainable components. + + :param edge_types: List containing strings for all possible edge types for the node type. + :return: None + """ + self.clear_submodules() + + ############################ + # Everything but Edges # + ############################ + self.create_node_models() + + ##################### + # Edge Encoders # + ##################### + if self.hyperparams["edge_encoding"]: + self.create_edge_models(edge_types) + + for name, module in self.node_modules.items(): + module.to(self.device) + + def create_new_scheduler(self, name, annealer, annealer_kws, creation_condition=True): + value_scheduler = None + rsetattr(self, name + "_scheduler", value_scheduler) + if creation_condition: + annealer_kws["device"] = self.device + value_annealer = annealer(annealer_kws) + rsetattr(self, name + "_annealer", value_annealer) + + # This is the value that we'll update on each call of + # step_annealers(). + rsetattr(self, name, value_annealer(0).clone().detach()) + dummy_optimizer = optim.Optimizer([rgetattr(self, name)], {"lr": value_annealer(0).clone().detach()}) + rsetattr(self, name + "_optimizer", dummy_optimizer) + + value_scheduler = CustomLR(dummy_optimizer, value_annealer) + rsetattr(self, name + "_scheduler", value_scheduler) + + self.schedulers.append(value_scheduler) + self.annealed_vars.append(name) + + def set_annealing_params(self): + self.schedulers = list() + self.annealed_vars = list() + + self.create_new_scheduler( + name="kl_weight", + annealer=sigmoid_anneal, + annealer_kws={ + "start": self.hyperparams["kl_weight_start"], + "finish": self.hyperparams["kl_weight"], + "center_step": self.hyperparams["kl_crossover"], + "steps_lo_to_hi": self.hyperparams["kl_crossover"] / self.hyperparams["kl_sigmoid_divisor"], + }, + ) + + self.create_new_scheduler( + name="latent.temp", + annealer=exp_anneal, + annealer_kws={ + "start": self.hyperparams["tau_init"], + "finish": self.hyperparams["tau_final"], + "rate": self.hyperparams["tau_decay_rate"], + }, + ) + + self.create_new_scheduler( + name="latent.z_logit_clip", + annealer=sigmoid_anneal, + annealer_kws={ + "start": self.hyperparams["z_logit_clip_start"], + "finish": self.hyperparams["z_logit_clip_final"], + "center_step": self.hyperparams["z_logit_clip_crossover"], + "steps_lo_to_hi": self.hyperparams["z_logit_clip_crossover"] / self.hyperparams["z_logit_clip_divisor"], + }, + creation_condition=self.hyperparams["use_z_logit_clipping"], + ) + + def step_annealers(self): + # This should manage all of the step-wise changed + # parameters automatically. + for idx, annealed_var in enumerate(self.annealed_vars): + if rgetattr(self, annealed_var + "_scheduler") is not None: + # First we step the scheduler. + with warnings.catch_warnings(): # We use a dummy optimizer: Warning because no .step() was called on it + warnings.simplefilter("ignore") + rgetattr(self, annealed_var + "_scheduler").step() + + # Then we set the annealed vars' value. + rsetattr(self, annealed_var, rgetattr(self, annealed_var + "_optimizer").param_groups[0]["lr"]) + + self.summarize_annealers() + + def summarize_annealers(self): + if self.log_writer is not None: + for annealed_var in self.annealed_vars: + if rgetattr(self, annealed_var) is not None: + self.log_writer.add_scalar( + "%s/%s" % (str(self.node_type), annealed_var.replace(".", "/")), + rgetattr(self, annealed_var), + self.curr_iter, + ) + + def obtain_encoded_tensors( + self, + mode, + inputs, + inputs_st, + packed_inputs_st, + labels, + labels_st, + first_history_indices, + neighbors, + neighbors_edge_value, + robot, + map, + ) -> (torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor): + """ + Encodes input and output tensors for node and robot. + + :param mode: Mode in which the model is operated. E.g. Train, Eval, Predict. + :param inputs: Input tensor including the state for each agent over time [bs, t, state]. + :param inputs_st: Standardized input tensor. + :param labels: Label tensor including the label output for each agent over time [bs, t, pred_state]. + :param labels_st: Standardized label tensor. + :param first_history_indices: First timestep (index) in scene for which data is available for a node [bs] + :param neighbors: Preprocessed dict (indexed by edge type) of list of neighbor states over time. + [[bs, t, neighbor state]] + :param neighbors_edge_value: Preprocessed edge values for all neighbor nodes [[N]] + :param robot: Standardized robot state over time. [bs, t, robot_state] + :param map: Tensor of Map information. [bs, channels, x, y] + :return: tuple(x, x_nr_t, y_e, y_r, y, n_s_t0) + WHERE + - x: Encoded input / condition tensor to the CVAE x_e. + - x_r_t: Robot state (if robot is in scene). + - y_e: Encoded label / future of the node. + - y_r: Encoded future of the robot. + - y: Label / future of the node. + - n_s_t0: Standardized current state of the node. + """ + + x, x_r_t, y_e, y_r, y = None, None, None, None, None + initial_dynamics = dict() + + batch_size = inputs.shape[0] + + ######################################### + # Provide basic information to encoders # + ######################################### + node_history = inputs + node_present_state = inputs[:, -1] + node_pos = inputs[:, -1, 0:2] + node_vel = inputs[:, -1, 2:4] + + node_history_st = packed_inputs_st + node_present_state_st = inputs_st[:, -1] + node_pos_st = inputs_st[:, -1, 0:2] + node_vel_st = inputs_st[:, -1, 2:4] + + n_s_t0 = node_present_state_st + + initial_dynamics["pos"] = node_pos + initial_dynamics["vel"] = node_vel + + self.dynamic.set_initial_condition(initial_dynamics) + + if self.hyperparams["incl_robot_node"]: + x_r_t, y_r = robot[..., 0, :], robot[..., 1:, :] + + ################## + # Encode History # + ################## + node_history_encoded = self.encode_node_history(mode, node_history_st, first_history_indices) + + return node_history_encoded + + ################## + # Encode Present # + ################## + node_present = node_present_state_st # [bs, state_dim] + + ################## + # Encode Future # + ################## + if mode != ModeKeys.PREDICT: + y = labels_st + + ############################## + # Encode Node Edges per Type # + ############################## + if self.hyperparams["edge_encoding"]: + node_edges_encoded = list() + for edge_type in self.edge_types: + # Encode edges for given edge type + encoded_edges_type = self.encode_edge( + mode, + node_history, + node_history_st, + edge_type, + neighbors[edge_type], + neighbors_edge_value[edge_type], + first_history_indices, + ) + node_edges_encoded.append(encoded_edges_type) # List of [bs/nbs, enc_rnn_dim] + ##################### + # Encode Node Edges # + ##################### + total_edge_influence = self.encode_total_edge_influence( + mode, node_edges_encoded, node_history_encoded, batch_size + ) + + ################ + # Map Encoding # + ################ + if self.hyperparams["use_map_encoding"] and self.node_type in self.hyperparams["map_encoder"]: + if self.log_writer and (self.curr_iter + 1) % 500 == 0: + map_clone = map.clone() + map_patch = self.hyperparams["map_encoder"][self.node_type]["patch_size"] + map_clone[:, :, map_patch[1] - 5 : map_patch[1] + 5, map_patch[0] - 5 : map_patch[0] + 5] = 1.0 + self.log_writer.add_images( + f"{self.node_type}/cropped_maps", map_clone, self.curr_iter, dataformats="NCWH" + ) + + encoded_map = self.node_modules[self.node_type + "/map_encoder"](map * 2.0 - 1.0, (mode == ModeKeys.TRAIN)) + do = self.hyperparams["map_encoder"][self.node_type]["dropout"] + encoded_map = F.dropout(encoded_map, do, training=(mode == ModeKeys.TRAIN)) + + ###################################### + # Concatenate Encoder Outputs into x # + ###################################### + x_concat_list = list() + + # Every node has an edge-influence encoder (which could just be zero). + if self.hyperparams["edge_encoding"]: + x_concat_list.append(total_edge_influence) # [bs/nbs, 4*enc_rnn_dim] + + # Every node has a history encoder. + x_concat_list.append(node_history_encoded) # [bs/nbs, enc_rnn_dim_history] + + if self.hyperparams["incl_robot_node"]: + robot_future_encoder = self.encode_robot_future(mode, x_r_t, y_r) + x_concat_list.append(robot_future_encoder) + + if self.hyperparams["use_map_encoding"] and self.node_type in self.hyperparams["map_encoder"]: + if self.log_writer: + self.log_writer.add_scalar( + f"{self.node_type}/encoded_map_max", torch.max(torch.abs(encoded_map)), self.curr_iter + ) + x_concat_list.append(encoded_map) + + x = torch.cat(x_concat_list, dim=1) + + if mode == ModeKeys.TRAIN or mode == ModeKeys.EVAL: + y_e = self.encode_node_future(mode, node_present, y) + + return x, x_r_t, y_e, y_r, y, n_s_t0 + + def encode_node_history(self, mode, node_hist, first_history_indices): + """ + Encodes the nodes history. + + :param mode: Mode in which the model is operated. E.g. Train, Eval, Predict. + :param node_hist: Historic and current state of the node. [bs, mhl, state] + :param first_history_indices: First timestep (index) in scene for which data is available for a node [bs] + :return: Encoded node history tensor. [bs, enc_rnn_dim] + """ + outputs = run_lstm_on_variable_length_seqs( + self.node_modules[self.node_type + "/node_history_encoder"], + # outputs, _ = run_lstm_on_variable_length_seqs(self.node_modules[self.node_type + '/node_history_encoder'], + original_seqs=node_hist, + lower_indices=first_history_indices, + ) + + return outputs + + outputs = F.dropout( + outputs, p=1.0 - self.hyperparams["rnn_kwargs"]["dropout_keep_prob"], training=(mode == ModeKeys.TRAIN) + ) # [bs, max_time, enc_rnn_dim] + + last_index_per_sequence = -(first_history_indices + 1) + + return outputs[torch.arange(first_history_indices.shape[0]), last_index_per_sequence] + + def encode_edge( + self, mode, node_history, node_history_st, edge_type, neighbors, neighbors_edge_value, first_history_indices + ): + + max_hl = self.hyperparams["maximum_history_length"] + + edge_states_list = list() # list of [#of neighbors, max_ht, state_dim] + for i, neighbor_states in enumerate(neighbors): # Get neighbors for timestep in batch + if len(neighbor_states) == 0: # There are no neighbors for edge type # TODO necessary? + neighbor_state_length = int( + np.sum([len(entity_dims) for entity_dims in self.state[edge_type[1]].values()]) + ) + edge_states_list.append(torch.zeros((1, max_hl + 1, neighbor_state_length), device=self.device)) + else: + edge_states_list.append(torch.stack(neighbor_states, dim=0).to(self.device)) + + if self.hyperparams["edge_state_combine_method"] == "sum": + # Used in Structural-RNN to combine edges as well. + op_applied_edge_states_list = list() + for neighbors_state in edge_states_list: + op_applied_edge_states_list.append(torch.sum(neighbors_state, dim=0)) + combined_neighbors = torch.stack(op_applied_edge_states_list, dim=0) + if self.hyperparams["dynamic_edges"] == "yes": + # Should now be (bs, time, 1) + op_applied_edge_mask_list = list() + for edge_value in neighbors_edge_value: + op_applied_edge_mask_list.append( + torch.clamp(torch.sum(edge_value.to(self.device), dim=0, keepdim=True), max=1.0) + ) + combined_edge_masks = torch.stack(op_applied_edge_mask_list, dim=0) + + elif self.hyperparams["edge_state_combine_method"] == "max": + # Used in NLP, e.g. max over word embeddings in a sentence. + op_applied_edge_states_list = list() + for neighbors_state in edge_states_list: + op_applied_edge_states_list.append(torch.max(neighbors_state, dim=0)) + combined_neighbors = torch.stack(op_applied_edge_states_list, dim=0) + if self.hyperparams["dynamic_edges"] == "yes": + # Should now be (bs, time, 1) + op_applied_edge_mask_list = list() + for edge_value in neighbors_edge_value: + op_applied_edge_mask_list.append( + torch.clamp(torch.max(edge_value.to(self.device), dim=0, keepdim=True), max=1.0) + ) + combined_edge_masks = torch.stack(op_applied_edge_mask_list, dim=0) + + elif self.hyperparams["edge_state_combine_method"] == "mean": + # Used in NLP, e.g. mean over word embeddings in a sentence. + op_applied_edge_states_list = list() + for neighbors_state in edge_states_list: + op_applied_edge_states_list.append(torch.mean(neighbors_state, dim=0)) + combined_neighbors = torch.stack(op_applied_edge_states_list, dim=0) + if self.hyperparams["dynamic_edges"] == "yes": + # Should now be (bs, time, 1) + op_applied_edge_mask_list = list() + for edge_value in neighbors_edge_value: + op_applied_edge_mask_list.append( + torch.clamp(torch.mean(edge_value.to(self.device), dim=0, keepdim=True), max=1.0) + ) + combined_edge_masks = torch.stack(op_applied_edge_mask_list, dim=0) + + joint_history = torch.cat([combined_neighbors, node_history_st], dim=-1) + + outputs, _ = run_lstm_on_variable_length_seqs( + self.node_modules[DirectedEdge.get_str_from_types(*edge_type) + "/edge_encoder"], + original_seqs=joint_history, + lower_indices=first_history_indices, + ) + + outputs = F.dropout( + outputs, p=1.0 - self.hyperparams["rnn_kwargs"]["dropout_keep_prob"], training=(mode == ModeKeys.TRAIN) + ) # [bs, max_time, enc_rnn_dim] + + last_index_per_sequence = -(first_history_indices + 1) + ret = outputs[torch.arange(last_index_per_sequence.shape[0]), last_index_per_sequence] + if self.hyperparams["dynamic_edges"] == "yes": + return ret * combined_edge_masks + else: + return ret + + def encode_total_edge_influence(self, mode, encoded_edges, node_history_encoder, batch_size): + if self.hyperparams["edge_influence_combine_method"] == "sum": + stacked_encoded_edges = torch.stack(encoded_edges, dim=0) + combined_edges = torch.sum(stacked_encoded_edges, dim=0) + + elif self.hyperparams["edge_influence_combine_method"] == "mean": + stacked_encoded_edges = torch.stack(encoded_edges, dim=0) + combined_edges = torch.mean(stacked_encoded_edges, dim=0) + + elif self.hyperparams["edge_influence_combine_method"] == "max": + stacked_encoded_edges = torch.stack(encoded_edges, dim=0) + combined_edges = torch.max(stacked_encoded_edges, dim=0) + + elif self.hyperparams["edge_influence_combine_method"] == "bi-rnn": + if len(encoded_edges) == 0: + combined_edges = torch.zeros((batch_size, self.eie_output_dims), device=self.device) + + else: + # axis=1 because then we get size [batch_size, max_time, depth] + encoded_edges = torch.stack(encoded_edges, dim=1) + + _, state = self.node_modules[self.node_type + "/edge_influence_encoder"](encoded_edges) + combined_edges = unpack_RNN_state(state) + combined_edges = F.dropout( + combined_edges, + p=1.0 - self.hyperparams["rnn_kwargs"]["dropout_keep_prob"], + training=(mode == ModeKeys.TRAIN), + ) + + elif self.hyperparams["edge_influence_combine_method"] == "attention": + # Used in Social Attention (https://arxiv.org/abs/1710.04689) + if len(encoded_edges) == 0: + combined_edges = torch.zeros((batch_size, self.eie_output_dims), device=self.device) + + else: + # axis=1 because then we get size [batch_size, max_time, depth] + encoded_edges = torch.stack(encoded_edges, dim=1) + combined_edges, _ = self.node_modules[self.node_type + "/edge_influence_encoder"]( + encoded_edges, node_history_encoder + ) + combined_edges = F.dropout( + combined_edges, + p=1.0 - self.hyperparams["rnn_kwargs"]["dropout_keep_prob"], + training=(mode == ModeKeys.TRAIN), + ) + + return combined_edges + + def encode_node_future(self, mode, node_present, node_future) -> torch.Tensor: + """ + Encodes the node future (during training) using a bi-directional LSTM + + :param mode: Mode in which the model is operated. E.g. Train, Eval, Predict. + :param node_present: Current state of the node. [bs, state] + :param node_future: Future states of the node. [bs, ph, state] + :return: Encoded future. + """ + initial_h_model = self.node_modules[self.node_type + "/node_future_encoder/initial_h"] + initial_c_model = self.node_modules[self.node_type + "/node_future_encoder/initial_c"] + + # Here we're initializing the forward hidden states, + # but zeroing the backward ones. + initial_h = initial_h_model(node_present) + initial_h = torch.stack([initial_h, torch.zeros_like(initial_h, device=self.device)], dim=0) + + initial_c = initial_c_model(node_present) + initial_c = torch.stack([initial_c, torch.zeros_like(initial_c, device=self.device)], dim=0) + + initial_state = (initial_h, initial_c) + + _, state = self.node_modules[self.node_type + "/node_future_encoder"](node_future, initial_state) + state = unpack_RNN_state(state) + state = F.dropout( + state, p=1.0 - self.hyperparams["rnn_kwargs"]["dropout_keep_prob"], training=(mode == ModeKeys.TRAIN) + ) + + return state + + def encode_robot_future(self, mode, robot_present, robot_future) -> torch.Tensor: + """ + Encodes the robot future (during training) using a bi-directional LSTM + + :param mode: Mode in which the model is operated. E.g. Train, Eval, Predict. + :param robot_present: Current state of the robot. [bs, state] + :param robot_future: Future states of the robot. [bs, ph, state] + :return: Encoded future. + """ + initial_h_model = self.node_modules["robot_future_encoder/initial_h"] + initial_c_model = self.node_modules["robot_future_encoder/initial_c"] + + # Here we're initializing the forward hidden states, + # but zeroing the backward ones. + initial_h = initial_h_model(robot_present) + initial_h = torch.stack([initial_h, torch.zeros_like(initial_h, device=self.device)], dim=0) + + initial_c = initial_c_model(robot_present) + initial_c = torch.stack([initial_c, torch.zeros_like(initial_c, device=self.device)], dim=0) + + initial_state = (initial_h, initial_c) + + _, state = self.node_modules["robot_future_encoder"](robot_future, initial_state) + state = unpack_RNN_state(state) + state = F.dropout( + state, p=1.0 - self.hyperparams["rnn_kwargs"]["dropout_keep_prob"], training=(mode == ModeKeys.TRAIN) + ) + + return state + + def q_z_xy(self, mode, x, y_e) -> torch.Tensor: + r""" + .. math:: q_\phi(z \mid \mathbf{x}_i, \mathbf{y}_i) + + :param mode: Mode in which the model is operated. E.g. Train, Eval, Predict. + :param x: Input / Condition tensor. + :param y_e: Encoded future tensor. + :return: Latent distribution of the CVAE. + """ + xy = torch.cat([x, y_e], dim=1) + + if self.hyperparams["q_z_xy_MLP_dims"] is not None: + dense = self.node_modules[self.node_type + "/q_z_xy"] + h = F.dropout( + F.relu(dense(xy)), p=1.0 - self.hyperparams["MLP_dropout_keep_prob"], training=(mode == ModeKeys.TRAIN) + ) + + else: + h = xy + + to_latent = self.node_modules[self.node_type + "/hxy_to_z"] + return self.latent.dist_from_h(to_latent(h), mode) + + def p_z_x(self, mode, x): + r""" + .. math:: p_\theta(z \mid \mathbf{x}_i) + + :param mode: Mode in which the model is operated. E.g. Train, Eval, Predict. + :param x: Input / Condition tensor. + :return: Latent distribution of the CVAE. + """ + if self.hyperparams["p_z_x_MLP_dims"] is not None: + dense = self.node_modules[self.node_type + "/p_z_x"] + h = F.dropout( + F.relu(dense(x)), p=1.0 - self.hyperparams["MLP_dropout_keep_prob"], training=(mode == ModeKeys.TRAIN) + ) + + else: + h = x + + to_latent = self.node_modules[self.node_type + "/hx_to_z"] + return self.latent.dist_from_h(to_latent(h), mode) + + def project_to_GMM_params(self, tensor) -> (torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor): + """ + Projects tensor to parameters of a GMM with N components and D dimensions. + + :param tensor: Input tensor. + :return: tuple(log_pis, mus, log_sigmas, corrs) + WHERE + - log_pis: Weight (logarithm) of each GMM component. [N] + - mus: Mean of each GMM component. [N, D] + - log_sigmas: Standard Deviation (logarithm) of each GMM component. [N, D] + - corrs: Correlation between the GMM components. [N] + """ + log_pis = self.node_modules[self.node_type + "/decoder/proj_to_GMM_log_pis"](tensor) + mus = self.node_modules[self.node_type + "/decoder/proj_to_GMM_mus"](tensor) + log_sigmas = self.node_modules[self.node_type + "/decoder/proj_to_GMM_log_sigmas"](tensor) + corrs = torch.tanh(self.node_modules[self.node_type + "/decoder/proj_to_GMM_corrs"](tensor)) + return log_pis, mus, log_sigmas, corrs + + def p_y_xz( + self, mode, x, x_nr_t, y_r, n_s_t0, z_stacked, prediction_horizon, num_samples, num_components=1, gmm_mode=False + ): + r""" + .. math:: p_\psi(\mathbf{y}_i \mid \mathbf{x}_i, z) + + :param mode: Mode in which the model is operated. E.g. Train, Eval, Predict. + :param x: Input / Condition tensor. + :param x_nr_t: Joint state of node and robot (if robot is in scene). + :param y: Future tensor. + :param y_r: Encoded future tensor. + :param n_s_t0: Standardized current state of the node. + :param z_stacked: Stacked latent state. [num_samples_z * num_samples_gmm, bs, latent_state] + :param prediction_horizon: Number of prediction timesteps. + :param num_samples: Number of samples from the latent space. + :param num_components: Number of GMM components. + :param gmm_mode: If True: The mode of the GMM is sampled. + :return: GMM2D. If mode is Predict, also samples from the GMM. + """ + ph = prediction_horizon + pred_dim = self.pred_state_length + + z = torch.reshape(z_stacked, (-1, self.latent.z_dim)) + zx = torch.cat([z, x.repeat(num_samples * num_components, 1)], dim=1) + + cell = self.node_modules[self.node_type + "/decoder/rnn_cell"] + initial_h_model = self.node_modules[self.node_type + "/decoder/initial_h"] + + initial_state = initial_h_model(zx) + + log_pis, mus, log_sigmas, corrs, a_sample = [], [], [], [], [] + + # Infer initial action state for node from current state + a_0 = self.node_modules[self.node_type + "/decoder/state_action"](n_s_t0) + + state = initial_state + if self.hyperparams["incl_robot_node"]: + input_ = torch.cat( + [zx, a_0.repeat(num_samples * num_components, 1), x_nr_t.repeat(num_samples * num_components, 1)], dim=1 + ) + else: + input_ = torch.cat([zx, a_0.repeat(num_samples * num_components, 1)], dim=1) + + for j in range(ph): + h_state = cell(input_, state) + log_pi_t, mu_t, log_sigma_t, corr_t = self.project_to_GMM_params(h_state) + + gmm = GMM2D(log_pi_t, mu_t, log_sigma_t, corr_t) # [k;bs, pred_dim] + + if mode == ModeKeys.PREDICT and gmm_mode: + a_t = gmm.mode() + else: + a_t = gmm.rsample() + + if num_components > 1: + if mode == ModeKeys.PREDICT: + log_pis.append(self.latent.p_dist.logits.repeat(num_samples, 1, 1)) + else: + log_pis.append(self.latent.q_dist.logits.repeat(num_samples, 1, 1)) + else: + log_pis.append( + torch.ones_like(corr_t.reshape(num_samples, num_components, -1).permute(0, 2, 1).reshape(-1, 1)) + ) + + mus.append( + mu_t.reshape(num_samples, num_components, -1, 2).permute(0, 2, 1, 3).reshape(-1, 2 * num_components) + ) + log_sigmas.append( + log_sigma_t.reshape(num_samples, num_components, -1, 2) + .permute(0, 2, 1, 3) + .reshape(-1, 2 * num_components) + ) + corrs.append(corr_t.reshape(num_samples, num_components, -1).permute(0, 2, 1).reshape(-1, num_components)) + + if self.hyperparams["incl_robot_node"]: + dec_inputs = [zx, a_t, y_r[:, j].repeat(num_samples * num_components, 1)] + else: + dec_inputs = [zx, a_t] + input_ = torch.cat(dec_inputs, dim=1) + state = h_state + + log_pis = torch.stack(log_pis, dim=1) + mus = torch.stack(mus, dim=1) + log_sigmas = torch.stack(log_sigmas, dim=1) + corrs = torch.stack(corrs, dim=1) + + a_dist = GMM2D( + torch.reshape(log_pis, [num_samples, -1, ph, num_components]), + torch.reshape(mus, [num_samples, -1, ph, num_components * pred_dim]), + torch.reshape(log_sigmas, [num_samples, -1, ph, num_components * pred_dim]), + torch.reshape(corrs, [num_samples, -1, ph, num_components]), + ) + + if self.hyperparams["dynamic"][self.node_type]["distribution"]: + y_dist = self.dynamic.integrate_distribution(a_dist, x) + else: + y_dist = a_dist + + if mode == ModeKeys.PREDICT: + if gmm_mode: + a_sample = a_dist.mode() + else: + a_sample = a_dist.rsample() + sampled_future = self.dynamic.integrate_samples(a_sample, x) + return y_dist, sampled_future + else: + return y_dist + + def encoder(self, mode, x, y_e, num_samples=None): + """ + Encoder of the CVAE. + + :param mode: Mode in which the model is operated. E.g. Train, Eval, Predict. + :param x: Input / Condition tensor. + :param y_e: Encoded future tensor. + :param num_samples: Number of samples from the latent space during Prediction. + :return: tuple(z, kl_obj) + WHERE + - z: Samples from the latent space. + - kl_obj: KL Divergenze between q and p + """ + if mode == ModeKeys.TRAIN: + sample_ct = self.hyperparams["k"] + elif mode == ModeKeys.EVAL: + sample_ct = self.hyperparams["k_eval"] + elif mode == ModeKeys.PREDICT: + sample_ct = num_samples + if num_samples is None: + raise ValueError("num_samples cannot be None with mode == PREDICT.") + + self.latent.q_dist = self.q_z_xy(mode, x, y_e) + self.latent.p_dist = self.p_z_x(mode, x) + + z = self.latent.sample_q(sample_ct, mode) + + if mode == ModeKeys.TRAIN: + kl_obj = self.latent.kl_q_p(self.log_writer, "%s" % str(self.node_type), self.curr_iter) + if self.log_writer is not None: + self.log_writer.add_scalar("%s/%s" % (str(self.node_type), "kl"), kl_obj, self.curr_iter) + else: + kl_obj = None + + return z, kl_obj + + def decoder(self, mode, x, x_nr_t, y, y_r, n_s_t0, z, labels, prediction_horizon, num_samples): + """ + Decoder of the CVAE. + + :param mode: Mode in which the model is operated. E.g. Train, Eval, Predict. + :param x: Input / Condition tensor. + :param x: Input / Condition tensor. + :param x_nr_t: Joint state of node and robot (if robot is in scene). + :param y: Future tensor. + :param y_r: Encoded future tensor. + :param n_s_t0: Standardized current state of the node. + :param z: Stacked latent state. + :param prediction_horizon: Number of prediction timesteps. + :param num_samples: Number of samples from the latent space. + :return: Log probability of y over p. + """ + + num_components = self.hyperparams["N"] * self.hyperparams["K"] + y_dist = self.p_y_xz( + mode, x, x_nr_t, y_r, n_s_t0, z, prediction_horizon, num_samples, num_components=num_components + ) + log_p_yt_xz = torch.clamp(y_dist.log_prob(labels), max=self.hyperparams["log_p_yt_xz_max"]) + if self.hyperparams["log_histograms"] and self.log_writer is not None: + self.log_writer.add_histogram("%s/%s" % (str(self.node_type), "log_p_yt_xz"), log_p_yt_xz, self.curr_iter) + + log_p_y_xz = torch.sum(log_p_yt_xz, dim=2) + return log_p_y_xz + + def train_loss( + self, + inputs, + inputs_st, + first_history_indices, + labels, + labels_st, + neighbors, + neighbors_edge_value, + robot, + map, + prediction_horizon, + ) -> torch.Tensor: + """ + Calculates the training loss for a batch. + + :param inputs: Input tensor including the state for each agent over time [bs, t, state]. + :param inputs_st: Standardized input tensor. + :param first_history_indices: First timestep (index) in scene for which data is available for a node [bs] + :param labels: Label tensor including the label output for each agent over time [bs, t, pred_state]. + :param labels_st: Standardized label tensor. + :param neighbors: Preprocessed dict (indexed by edge type) of list of neighbor states over time. + [[bs, t, neighbor state]] + :param neighbors_edge_value: Preprocessed edge values for all neighbor nodes [[N]] + :param robot: Standardized robot state over time. [bs, t, robot_state] + :param map: Tensor of Map information. [bs, channels, x, y] + :param prediction_horizon: Number of prediction timesteps. + :return: Scalar tensor -> nll loss + """ + mode = ModeKeys.TRAIN + + x, x_nr_t, y_e, y_r, y, n_s_t0 = self.obtain_encoded_tensors( + mode=mode, + inputs=inputs, + inputs_st=inputs_st, + labels=labels, + labels_st=labels_st, + first_history_indices=first_history_indices, + neighbors=neighbors, + neighbors_edge_value=neighbors_edge_value, + robot=robot, + map=map, + ) + + z, kl = self.encoder(mode, x, y_e) + log_p_y_xz = self.decoder( + mode, + x, + x_nr_t, + y, + y_r, + n_s_t0, + z, + labels, # Loss is calculated on unstandardized label + prediction_horizon, + self.hyperparams["k"], + ) + + log_p_y_xz_mean = torch.mean(log_p_y_xz, dim=0) # [nbs] + log_likelihood = torch.mean(log_p_y_xz_mean) + + mutual_inf_q = mutual_inf_mc(self.latent.q_dist) + mutual_inf_p = mutual_inf_mc(self.latent.p_dist) + + ELBO = log_likelihood - self.kl_weight * kl + 1.0 * mutual_inf_p + loss = -ELBO + + if self.hyperparams["log_histograms"] and self.log_writer is not None: + self.log_writer.add_histogram( + "%s/%s" % (str(self.node_type), "log_p_y_xz"), log_p_y_xz_mean, self.curr_iter + ) + + if self.log_writer is not None: + self.log_writer.add_scalar( + "%s/%s" % (str(self.node_type), "mutual_information_q"), mutual_inf_q, self.curr_iter + ) + self.log_writer.add_scalar( + "%s/%s" % (str(self.node_type), "mutual_information_p"), mutual_inf_p, self.curr_iter + ) + self.log_writer.add_scalar( + "%s/%s" % (str(self.node_type), "log_likelihood"), log_likelihood, self.curr_iter + ) + self.log_writer.add_scalar("%s/%s" % (str(self.node_type), "loss"), loss, self.curr_iter) + if self.hyperparams["log_histograms"]: + self.latent.summarize_for_tensorboard(self.log_writer, str(self.node_type), self.curr_iter) + return loss + + def eval_loss( + self, + inputs, + inputs_st, + first_history_indices, + labels, + labels_st, + neighbors, + neighbors_edge_value, + robot, + map, + prediction_horizon, + ) -> torch.Tensor: + """ + Calculates the evaluation loss for a batch. + + :param inputs: Input tensor including the state for each agent over time [bs, t, state]. + :param inputs_st: Standardized input tensor. + :param first_history_indices: First timestep (index) in scene for which data is available for a node [bs] + :param labels: Label tensor including the label output for each agent over time [bs, t, pred_state]. + :param labels_st: Standardized label tensor. + :param neighbors: Preprocessed dict (indexed by edge type) of list of neighbor states over time. + [[bs, t, neighbor state]] + :param neighbors_edge_value: Preprocessed edge values for all neighbor nodes [[N]] + :param robot: Standardized robot state over time. [bs, t, robot_state] + :param map: Tensor of Map information. [bs, channels, x, y] + :param prediction_horizon: Number of prediction timesteps. + :return: tuple(nll_q_is, nll_p, nll_exact, nll_sampled) + """ + + mode = ModeKeys.EVAL + + x, x_nr_t, y_e, y_r, y, n_s_t0 = self.obtain_encoded_tensors( + mode=mode, + inputs=inputs, + inputs_st=inputs_st, + labels=labels, + labels_st=labels_st, + first_history_indices=first_history_indices, + neighbors=neighbors, + neighbors_edge_value=neighbors_edge_value, + robot=robot, + map=map, + ) + + num_components = self.hyperparams["N"] * self.hyperparams["K"] + ### Importance sampled NLL estimate + z, _ = self.encoder(mode, x, y_e) # [k_eval, nbs, N*K] + z = self.latent.sample_p(1, mode, full_dist=True) + y_dist, _ = self.p_y_xz( + ModeKeys.PREDICT, + x, + x_nr_t, + y_r, + n_s_t0, + z, + prediction_horizon, + num_samples=1, + num_components=num_components, + ) + # We use unstandardized labels to compute the loss + log_p_yt_xz = torch.clamp(y_dist.log_prob(labels), max=self.hyperparams["log_p_yt_xz_max"]) + log_p_y_xz = torch.sum(log_p_yt_xz, dim=2) + log_p_y_xz_mean = torch.mean(log_p_y_xz, dim=0) # [nbs] + log_likelihood = torch.mean(log_p_y_xz_mean) + nll = -log_likelihood + + return nll + + def predict( + self, + inputs, + inputs_st, + packed_inputs_st, + first_history_indices, + neighbors, + neighbors_edge_value, + robot, + map, + prediction_horizon, + num_samples, + z_mode=False, + gmm_mode=False, + full_dist=True, + all_z_sep=False, + ): + """ + Predicts the future of a batch of nodes. + + :param inputs: Input tensor including the state for each agent over time [bs, t, state]. + :param inputs_st: Standardized input tensor. + :param first_history_indices: First timestep (index) in scene for which data is available for a node [bs] + :param neighbors: Preprocessed dict (indexed by edge type) of list of neighbor states over time. + [[bs, t, neighbor state]] + :param neighbors_edge_value: Preprocessed edge values for all neighbor nodes [[N]] + :param robot: Standardized robot state over time. [bs, t, robot_state] + :param map: Tensor of Map information. [bs, channels, x, y] + :param prediction_horizon: Number of prediction timesteps. + :param num_samples: Number of samples from the latent space. + :param z_mode: If True: Select the most likely latent state. + :param gmm_mode: If True: The mode of the GMM is sampled. + :param all_z_sep: Samples each latent mode individually without merging them into a GMM. + :param full_dist: Samples all latent states and merges them into a GMM as output. + :return: + """ + mode = ModeKeys.PREDICT + + # x, x_nr_t, _, y_r, _, n_s_t0 = self.obtain_encoded_tensors(mode=mode, + out = self.obtain_encoded_tensors( + mode=mode, + inputs=inputs, + inputs_st=inputs_st, + packed_inputs_st=packed_inputs_st, + labels=None, + labels_st=None, + first_history_indices=first_history_indices, + neighbors=neighbors, + neighbors_edge_value=neighbors_edge_value, + robot=robot, + map=map, + ) + # return x, n_s_t0 + return out + + self.latent.p_dist = self.p_z_x(mode, x) + z, num_samples, num_components = self.latent.sample_p( + num_samples, mode, most_likely_z=z_mode, full_dist=full_dist, all_z_sep=all_z_sep + ) + + _, our_sampled_future = self.p_y_xz( + mode, x, x_nr_t, y_r, n_s_t0, z, prediction_horizon, num_samples, num_components, gmm_mode + ) + + return our_sampled_future diff --git a/forge/test/models/pytorch/multimodal/trajectron/trajectron/model/model_registrar.py b/forge/test/models/pytorch/multimodal/trajectron/trajectron/model/model_registrar.py new file mode 100644 index 000000000..d5aaf1966 --- /dev/null +++ b/forge/test/models/pytorch/multimodal/trajectron/trajectron/model/model_registrar.py @@ -0,0 +1,76 @@ +# SPDX-FileCopyrightText: (c) 2024 Tenstorrent AI ULC +# +# SPDX-License-Identifier: Apache-2.0 +import os +import torch +import torch.nn as nn + + +def get_model_device(model): + return next(model.parameters()).device + + +class ModelRegistrar(nn.Module): + def __init__(self, model_dir, device): + super(ModelRegistrar, self).__init__() + self.model_dict = nn.ModuleDict() + self.model_dir = model_dir + self.device = device + + def forward(self): + raise NotImplementedError("Although ModelRegistrar is a nn.Module, it is only to store parameters.") + + def get_model(self, name, model_if_absent=None): + # 4 cases: name in self.model_dict and model_if_absent is None (OK) + # name in self.model_dict and model_if_absent is not None (OK) + # name not in self.model_dict and model_if_absent is not None (OK) + # name not in self.model_dict and model_if_absent is None (NOT OK) + + if name in self.model_dict: + return self.model_dict[name] + + elif model_if_absent is not None: + self.model_dict[name] = model_if_absent.to(self.device) + return self.model_dict[name] + + else: + raise ValueError(f"{name} was never initialized in this Registrar!") + + def get_name_match(self, name): + ret_model_list = nn.ModuleList() + for key in self.model_dict.keys(): + if name in key: + ret_model_list.append(self.model_dict[key]) + return ret_model_list + + def get_all_but_name_match(self, name): + ret_model_list = nn.ModuleList() + for key in self.model_dict.keys(): + if name not in key: + ret_model_list.append(self.model_dict[key]) + return ret_model_list + + def print_model_names(self): + print(self.model_dict.keys()) + + def save_models(self, curr_iter): + # Create the model directiory if it's not present. + save_path = os.path.join(self.model_dir, "model_registrar-%d.pt" % curr_iter) + + torch.save(self.model_dict, save_path) + + def load_models(self, iter_num): + self.model_dict.clear() + + save_path = os.path.join(self.model_dir, "model_registrar-%d.pt" % iter_num) + + print("") + print("Loading from " + save_path) + self.model_dict = torch.load(save_path, map_location=self.device) + print("Loaded!") + print("") + + def to(self, device): + for name, model in self.model_dict.items(): + if get_model_device(model) != device: + model.to(device) diff --git a/forge/test/models/pytorch/multimodal/trajectron/trajectron/model/model_utils.py b/forge/test/models/pytorch/multimodal/trajectron/trajectron/model/model_utils.py new file mode 100644 index 000000000..19c4cf3ec --- /dev/null +++ b/forge/test/models/pytorch/multimodal/trajectron/trajectron/model/model_utils.py @@ -0,0 +1,137 @@ +# SPDX-FileCopyrightText: (c) 2024 Tenstorrent AI ULC +# +# SPDX-License-Identifier: Apache-2.0 +import torch +import torch.nn.utils.rnn as rnn +from enum import Enum +import functools +import numpy as np +import math + + +class ModeKeys(Enum): + TRAIN = 1 + EVAL = 2 + PREDICT = 3 + + +def cyclical_lr(stepsize, min_lr=3e-4, max_lr=3e-3, decay=1.0): + # Lambda function to calculate the LR + lr_lambda = lambda it: min_lr + (max_lr - min_lr) * relative(it, stepsize) * decay**it + + # Additional function to see where on the cycle we are + def relative(it, stepsize): + cycle = math.floor(1 + it / (2 * stepsize)) + x = abs(it / stepsize - 2 * cycle + 1) + return max(0, (1 - x)) + + return lr_lambda + + +def to_one_hot(labels, n_labels): + return torch.eye(n_labels, device=labels.device)[labels] + + +def exp_anneal(anneal_kws): + device = anneal_kws["device"] + start = torch.tensor(anneal_kws["start"], device=device) + finish = torch.tensor(anneal_kws["finish"], device=device) + rate = torch.tensor(anneal_kws["rate"], device=device) + return lambda step: finish - (finish - start) * torch.pow( + rate, torch.tensor(step, dtype=torch.float, device=device) + ) + + +def sigmoid_anneal(anneal_kws): + device = anneal_kws["device"] + start = torch.tensor(anneal_kws["start"], device=device) + finish = torch.tensor(anneal_kws["finish"], device=device) + center_step = torch.tensor(anneal_kws["center_step"], device=device, dtype=torch.float) + steps_lo_to_hi = torch.tensor(anneal_kws["steps_lo_to_hi"], device=device, dtype=torch.float) + return lambda step: start + (finish - start) * torch.sigmoid( + (torch.tensor(float(step), device=device) - center_step) * (1.0 / steps_lo_to_hi) + ) + + +class CustomLR(torch.optim.lr_scheduler.LambdaLR): + def __init__(self, optimizer, lr_lambda, last_epoch=-1): + super(CustomLR, self).__init__(optimizer, lr_lambda, last_epoch) + + def get_lr(self): + return [lmbda(self.last_epoch) for lmbda, base_lr in zip(self.lr_lambdas, self.base_lrs)] + + +def mutual_inf_mc(x_dist): + dist = x_dist.__class__ + H_y = dist(probs=x_dist.probs.mean(dim=0)).entropy() + return (H_y - x_dist.entropy().mean(dim=0)).sum() + + +def run_lstm_on_variable_length_seqs( + lstm_module, original_seqs, lower_indices=None, upper_indices=None, total_length=None +): + # breakpoint() + # bs, tf = original_seqs.shape[:2] + # if lower_indices is None: + # lower_indices = torch.zeros(bs, dtype=torch.int) + # if upper_indices is None: + # upper_indices = torch.ones(bs, dtype=torch.int) * (tf - 1) + # if total_length is None: + # total_length = max(upper_indices) + 1 + # # This is done so that we can just pass in self.prediction_timesteps + # # (which we want to INCLUDE, so this will exclude the next timestep). + # inclusive_break_indices = upper_indices + 1 + + # pad_list = list() + # for i, seq_len in enumerate(inclusive_break_indices): + # pad_list.append(original_seqs[i, lower_indices[i]:seq_len]) + + # packed_seqs = rnn.pack_sequence(pad_list, enforce_sorted=False) + # return packed_seqs # TypeError: int() argument must be a string, a bytes-like object or a real number, not 'Any' + + packed_seqs = original_seqs + packed_output, (h_n, c_n) = lstm_module(packed_seqs) + return packed_output # TypeError: object of type 'Call' has no len() + output, _ = rnn.pad_packed_sequence(packed_output, batch_first=True, total_length=total_length) + + return output, (h_n, c_n) + + +def extract_subtensor_per_batch_element(tensor, indices): + batch_idxs = torch.arange(start=0, end=len(indices)) + + batch_idxs = batch_idxs[~torch.isnan(indices)] + indices = indices[~torch.isnan(indices)] + if indices.size == 0: + return None + else: + indices = indices.long() + if tensor.is_cuda: + batch_idxs = batch_idxs.to(tensor.get_device()) + indices = indices.to(tensor.get_device()) + return tensor[batch_idxs, indices] + + +def unpack_RNN_state(state_tuple): + # PyTorch returned LSTM states have 3 dims: + # (num_layers * num_directions, batch, hidden_size) + + state = torch.cat(state_tuple, dim=0).permute(1, 0, 2) + # Now state is (batch, 2 * num_layers * num_directions, hidden_size) + + state_size = state.size() + return torch.reshape(state, (-1, state_size[1] * state_size[2])) + + +def rsetattr(obj, attr, val): + pre, _, post = attr.rpartition(".") + return setattr(rgetattr(obj, pre) if pre else obj, post, val) + + +# using wonder's beautiful simplification: +# https://stackoverflow.com/questions/31174295/getattr-and-setattr-on-nested-objects/31174427?noredirect=1#comment86638618_31174427 +def rgetattr(obj, attr, *args): + def _getattr(obj, attr): + return getattr(obj, attr, *args) + + return functools.reduce(_getattr, [obj] + attr.split(".")) diff --git a/forge/test/models/pytorch/multimodal/trajectron/trajectron/model/online/__init__.py b/forge/test/models/pytorch/multimodal/trajectron/trajectron/model/online/__init__.py new file mode 100644 index 000000000..e8fa6b337 --- /dev/null +++ b/forge/test/models/pytorch/multimodal/trajectron/trajectron/model/online/__init__.py @@ -0,0 +1,5 @@ +# SPDX-FileCopyrightText: (c) 2024 Tenstorrent AI ULC +# +# SPDX-License-Identifier: Apache-2.0 +from .online_trajectron import OnlineTrajectron +from .online_mgcvae import OnlineMultimodalGenerativeCVAE diff --git a/forge/test/models/pytorch/multimodal/trajectron/trajectron/model/online/online_mgcvae.py b/forge/test/models/pytorch/multimodal/trajectron/trajectron/model/online/online_mgcvae.py new file mode 100644 index 000000000..624ebf426 --- /dev/null +++ b/forge/test/models/pytorch/multimodal/trajectron/trajectron/model/online/online_mgcvae.py @@ -0,0 +1,428 @@ +# SPDX-FileCopyrightText: (c) 2024 Tenstorrent AI ULC +# +# SPDX-License-Identifier: Apache-2.0 +import warnings +import torch +import torch.nn as nn +import torch.nn.functional as F +import numpy as np +from collections import defaultdict, Counter +from model.components import * +from model.model_utils import * +from model.dataset import get_relative_robot_traj +import model.dynamics as dynamic_module +from model.mgcvae import MultimodalGenerativeCVAE +from environment.scene_graph import DirectedEdge +from environment.node_type import NodeType + + +class OnlineMultimodalGenerativeCVAE(MultimodalGenerativeCVAE): + def __init__(self, env, node, model_registrar, hyperparams, device): + self.hyperparams = hyperparams + self.node = node + self.node_type = self.node.type + + if len(env.scenes) != 1: + raise ValueError("Passed in Environment has number of scenes != 1") + self.robot = env.scenes[0].robot + self.model_registrar = model_registrar + self.device = device + + self.node_modules = dict() + self.env = env + self.scene_graph = None + + self.state = self.hyperparams["state"] + self.pred_state = self.hyperparams["pred_state"][self.node.type] + self.state_length = int(np.sum([len(entity_dims) for entity_dims in self.state[self.node.type].values()])) + if self.hyperparams["incl_robot_node"]: + self.robot_state_length = int( + np.sum([len(entity_dims) for entity_dims in self.state[self.robot.type].values()]) + ) + self.pred_state_length = int(np.sum([len(entity_dims) for entity_dims in self.pred_state.values()])) + + self.curr_hidden_states = dict() + self.edge_types = Counter() + + self.create_graphical_model() + + dynamic_class = getattr(dynamic_module, self.hyperparams["dynamic"][self.node_type]["name"]) + dyn_limits = hyperparams["dynamic"][self.node_type]["limits"] + self.dynamic = dynamic_class( + self.env.scenes[0].dt, dyn_limits, device, self.model_registrar, self.x_size, self.node_type + ) + + def create_graphical_model(self): + """ + Creates or queries all trainable components. + + :return: None + """ + self.clear_submodules() + + ############################ + # Everything but Edges # + ############################ + self.create_node_models() + + for name, module in self.node_modules.items(): + module.to(self.device) + + def update_graph(self, new_scene_graph, new_neighbors, removed_neighbors): + self.scene_graph = new_scene_graph + + if self.node in new_neighbors: + for edge_type, new_neighbor_nodes in new_neighbors[self.node].items(): + self.add_edge_model(edge_type) + self.edge_types += Counter({edge_type: len(new_neighbor_nodes)}) + + if self.node in removed_neighbors: + for edge_type, removed_neighbor_nodes in removed_neighbors[self.node].items(): + self.remove_edge_model(edge_type) + self.edge_types -= Counter({edge_type: len(removed_neighbor_nodes)}) + + def get_edge_to(self, other_node): + return DirectedEdge(self.node, other_node) + + def add_edge_model(self, edge_type): + if self.hyperparams["edge_encoding"]: + if edge_type + "/edge_encoder" not in self.node_modules: + neighbor_state_length = int( + np.sum( + [ + len(entity_dims) + for entity_dims in self.state[self._get_other_node_type_from_edge(edge_type)].values() + ] + ) + ) + if self.hyperparams["edge_state_combine_method"] == "pointnet": + self.add_submodule( + edge_type + "/pointnet_encoder", + model_if_absent=nn.Sequential( + nn.Linear(self.state_length, 2 * self.state_length), + nn.ReLU(), + nn.Linear(2 * self.state_length, 2 * self.state_length), + nn.ReLU(), + ), + ) + + edge_encoder_input_size = 2 * self.state_length + self.state_length + + elif self.hyperparams["edge_state_combine_method"] == "attention": + self.add_submodule( + self.node.type + "/edge_attention_combine", + model_if_absent=TemporallyBatchedAdditiveAttention( + encoder_hidden_state_dim=self.state_length, decoder_hidden_state_dim=self.state_length + ), + ) + edge_encoder_input_size = self.state_length + neighbor_state_length + + else: + edge_encoder_input_size = self.state_length + neighbor_state_length + + self.add_submodule( + edge_type + "/edge_encoder", + model_if_absent=nn.LSTM( + input_size=edge_encoder_input_size, + hidden_size=self.hyperparams["enc_rnn_dim_edge"], + batch_first=True, + ), + ) + + def _get_other_node_type_from_edge(self, edge_type_str): + n2_type_str = edge_type_str.split("->")[1] + return NodeType(n2_type_str, self.env.node_type_list.index(n2_type_str) + 1) + + def _get_edge_type_from_str(self, edge_type_str): + n1_type_str, n2_type_str = edge_type_str.split("->") + return ( + NodeType(n1_type_str, self.env.node_type_list.index(n1_type_str) + 1), + NodeType(n2_type_str, self.env.node_type_list.index(n2_type_str) + 1), + ) + + def remove_edge_model(self, edge_type): + if self.hyperparams["edge_encoding"]: + if len(self.scene_graph.get_neighbors(self.node, self._get_other_node_type_from_edge(edge_type))) == 0: + del self.node_modules[edge_type + "/edge_encoder"] + + def obtain_encoded_tensors(self, mode, inputs, inputs_st, inputs_np, robot_present_and_future, maps): + x, x_r_t, y_r = None, None, None + batch_size = 1 + + our_inputs = inputs[self.node] + our_inputs_st = inputs_st[self.node] + + initial_dynamics = dict() + initial_dynamics["pos"] = our_inputs[:, 0:2] # TODO: Generalize + initial_dynamics["vel"] = our_inputs[:, 2:4] # TODO: Generalize + self.dynamic.set_initial_condition(initial_dynamics) + + ######################################### + # Provide basic information to encoders # + ######################################### + if self.hyperparams["incl_robot_node"] and self.robot is not None: + robot_present_and_future_st = get_relative_robot_traj( + self.env, self.state, our_inputs, robot_present_and_future, self.node.type, self.robot.type + ) + x_r_t = robot_present_and_future_st[..., 0, :] + y_r = robot_present_and_future_st[..., 1:, :] + + ################## + # Encode History # + ################## + node_history_encoded = self.encode_node_history(our_inputs_st) + + ############################## + # Encode Node Edges per Type # + ############################## + total_edge_influence = None + if self.hyperparams["edge_encoding"]: + node_edges_encoded = list() + for edge_type in self.edge_types: + connected_nodes_batched = list() + edge_masks_batched = list() + + # We get all nodes which are connected to the current node for the current timestep + connected_nodes_batched.append( + self.scene_graph.get_neighbors(self.node, self._get_other_node_type_from_edge(edge_type)) + ) + + if self.hyperparams["dynamic_edges"] == "yes": + # We get the edge masks for the current node at the current timestep + edge_masks_for_node = self.scene_graph.get_edge_scaling(self.node) + edge_masks_batched.append(torch.tensor(edge_masks_for_node, dtype=torch.float, device=self.device)) + + # Encode edges for given edge type + encoded_edges_type = self.encode_edge( + inputs, inputs_st, inputs_np, edge_type, connected_nodes_batched, edge_masks_batched + ) + node_edges_encoded.append(encoded_edges_type) # List of [bs/nbs, enc_rnn_dim] + + ##################### + # Encode Node Edges # + ##################### + total_edge_influence = self.encode_total_edge_influence( + mode, node_edges_encoded, node_history_encoded, batch_size + ) + + self.TD = {"node_history_encoded": node_history_encoded, "total_edge_influence": total_edge_influence} + + ################ + # Map Encoding # + ################ + if self.hyperparams["use_map_encoding"] and self.node_type in self.hyperparams["map_encoder"]: + if self.node not in maps: + # This means the node was removed (it is only being kept around because of the edge removal filter). + me_params = self.hyperparams["map_encoder"][self.node_type] + self.TD["encoded_map"] = torch.zeros((1, me_params["output_size"])) + else: + encoded_map = self.node_modules[self.node_type + "/map_encoder"]( + maps[self.node] * 2.0 - 1.0, (mode == ModeKeys.TRAIN) + ) + do = self.hyperparams["map_encoder"][self.node_type]["dropout"] + encoded_map = F.dropout(encoded_map, do, training=(mode == ModeKeys.TRAIN)) + self.TD["encoded_map"] = encoded_map + + ###################################### + # Concatenate Encoder Outputs into x # + ###################################### + return self.create_encoder_rep(mode, self.TD, x_r_t, y_r) + + def create_encoder_rep(self, mode, TD, robot_present_st, robot_future_st): + # Unpacking TD + node_history_encoded = TD["node_history_encoded"] + if self.hyperparams["edge_encoding"]: + total_edge_influence = TD["total_edge_influence"] + if self.hyperparams["use_map_encoding"] and self.node_type in self.hyperparams["map_encoder"]: + encoded_map = TD["encoded_map"] + + if ( + self.hyperparams["incl_robot_node"] + and self.robot is not None + and robot_future_st is not None + and robot_present_st is not None + ): + robot_future_encoder = self.encode_robot_future(mode, robot_present_st, robot_future_st) + + # Tiling for multiple samples + # This tiling is done because: + # a) we must consider the prediction case where there are many candidate robot future actions, + # b) the edge and history encoders are all the same regardless of which candidate future robot action + # we're evaluating. + node_history_encoded = TD["node_history_encoded"].repeat(robot_future_st.size()[0], 1) + if self.hyperparams["edge_encoding"]: + total_edge_influence = TD["total_edge_influence"].repeat(robot_future_st.size()[0], 1) + if self.hyperparams["use_map_encoding"] and self.node_type in self.hyperparams["map_encoder"]: + encoded_map = TD["encoded_map"].repeat(robot_future_st.size()[0], 1) + + elif self.hyperparams["incl_robot_node"] and self.robot is not None: + # Four times because we're trying to mimic a bi-directional RNN's output (which is c and h from both ends). + robot_future_encoder = torch.zeros([1, 4 * self.hyperparams["enc_rnn_dim_future"]], device=self.device) + + x_concat_list = list() + + # Every node has an edge-influence encoder (which could just be zero). + if self.hyperparams["edge_encoding"]: + x_concat_list.append(total_edge_influence) # [bs/nbs, 4*enc_rnn_dim] + + # Every node has a history encoder. + x_concat_list.append(node_history_encoded) # [bs/nbs, enc_rnn_dim_history] + + if self.hyperparams["incl_robot_node"] and self.robot is not None: + x_concat_list.append(robot_future_encoder) # [bs/nbs, 4*enc_rnn_dim_history] + + if self.hyperparams["use_map_encoding"] and self.node_type in self.hyperparams["map_encoder"]: + x_concat_list.append(encoded_map) # [bs/nbs, CNN output size] + + return torch.cat(x_concat_list, dim=1) + + def encode_node_history(self, inputs_st): + new_state = torch.unsqueeze(inputs_st, dim=1) # [bs, 1, state_dim] + if self.node.type + "/node_history_encoder" not in self.curr_hidden_states: + outputs, self.curr_hidden_states[self.node.type + "/node_history_encoder"] = self.node_modules[ + self.node.type + "/node_history_encoder" + ](new_state) + else: + outputs, self.curr_hidden_states[self.node.type + "/node_history_encoder"] = self.node_modules[ + self.node.type + "/node_history_encoder" + ](new_state, self.curr_hidden_states[self.node.type + "/node_history_encoder"]) + + return outputs[:, 0, :] + + def encode_edge(self, inputs, inputs_st, inputs_np, edge_type, connected_nodes, edge_masks): + edge_type_tuple = self._get_edge_type_from_str(edge_type) + edge_states_list = list() # list of [#of neighbors, max_ht, state_dim] + neighbor_states = list() + + orig_rel_state = inputs[self.node].cpu().numpy() + for node in connected_nodes[0]: + neighbor_state_np = inputs_np[node] + + # Make State relative to node + _, std = self.env.get_standardize_params(self.state[node.type], node_type=node.type) + std[0:2] = self.env.attention_radius[edge_type_tuple] + + # TODO: This all makes the unsafe assumption that the first n dims + # refer to the same quantities even for different agent types! + equal_dims = np.min((neighbor_state_np.shape[-1], orig_rel_state.shape[-1])) + rel_state = np.zeros_like(neighbor_state_np) + rel_state[..., :equal_dims] = orig_rel_state[..., :equal_dims] + neighbor_state_np_st = self.env.standardize( + neighbor_state_np, self.state[node.type], node_type=node.type, mean=rel_state, std=std + ) + + neighbor_state = torch.tensor(neighbor_state_np_st).float().to(self.device) + neighbor_states.append(neighbor_state) + + if len(neighbor_states) == 0: # There are no neighbors for edge type # TODO necessary? + neighbor_state_length = int(np.sum([len(entity_dims) for entity_dims in self.state[edge_type[1]].values()])) + edge_states_list.append(torch.zeros((1, 1, neighbor_state_length), device=self.device)) + else: + edge_states_list.append(torch.stack(neighbor_states, dim=0)) + + if self.hyperparams["edge_state_combine_method"] == "sum": + # Used in Structural-RNN to combine edges as well. + op_applied_edge_states_list = list() + for neighbors_state in edge_states_list: + op_applied_edge_states_list.append(torch.sum(neighbors_state, dim=0)) + combined_neighbors = torch.stack(op_applied_edge_states_list, dim=0) + if self.hyperparams["dynamic_edges"] == "yes": + # Should now be (bs, time, 1) + op_applied_edge_mask_list = list() + for edge_mask in edge_masks: + op_applied_edge_mask_list.append(torch.clamp(torch.sum(edge_mask, dim=0, keepdim=True), max=1.0)) + combined_edge_masks = torch.stack(op_applied_edge_mask_list, dim=0) + + elif self.hyperparams["edge_state_combine_method"] == "max": + # Used in NLP, e.g. max over word embeddings in a sentence. + op_applied_edge_states_list = list() + for neighbors_state in edge_states_list: + op_applied_edge_states_list.append(torch.max(neighbors_state, dim=0)) + combined_neighbors = torch.stack(op_applied_edge_states_list, dim=0) + if self.hyperparams["dynamic_edges"] == "yes": + # Should now be (bs, time, 1) + op_applied_edge_mask_list = list() + for edge_mask in edge_masks: + op_applied_edge_mask_list.append(torch.clamp(torch.max(edge_mask, dim=0, keepdim=True), max=1.0)) + combined_edge_masks = torch.stack(op_applied_edge_mask_list, dim=0) + + elif self.hyperparams["edge_state_combine_method"] == "mean": + # Used in NLP, e.g. mean over word embeddings in a sentence. + op_applied_edge_states_list = list() + for neighbors_state in edge_states_list: + op_applied_edge_states_list.append(torch.mean(neighbors_state, dim=0)) + combined_neighbors = torch.stack(op_applied_edge_states_list, dim=0) + if self.hyperparams["dynamic_edges"] == "yes": + # Should now be (bs, time, 1) + op_applied_edge_mask_list = list() + for edge_mask in edge_masks: + op_applied_edge_mask_list.append(torch.clamp(torch.mean(edge_mask, dim=0, keepdim=True), max=1.0)) + combined_edge_masks = torch.stack(op_applied_edge_mask_list, dim=0) + + joint_history = torch.cat([combined_neighbors, torch.unsqueeze(inputs_st[self.node], dim=0)], dim=-1) + + if edge_type + "/edge_encoder" not in self.curr_hidden_states: + outputs, self.curr_hidden_states[edge_type + "/edge_encoder"] = self.node_modules[ + edge_type + "/edge_encoder" + ](joint_history) + else: + outputs, self.curr_hidden_states[edge_type + "/edge_encoder"] = self.node_modules[ + edge_type + "/edge_encoder" + ](joint_history, self.curr_hidden_states[edge_type + "/edge_encoder"]) + + if self.hyperparams["dynamic_edges"] == "yes": + return outputs[:, 0, :] * combined_edge_masks + else: + return outputs[:, 0, :] # [bs, enc_rnn_dim] + + def encoder_forward(self, inputs, inputs_st, inputs_np, robot_present_and_future=None, maps=None): + # Always predicting with the online model. + mode = ModeKeys.PREDICT + + self.x = self.obtain_encoded_tensors(mode, inputs, inputs_st, inputs_np, robot_present_and_future, maps) + self.n_s_t0 = inputs_st[self.node] + + self.latent.p_dist = self.p_z_x(mode, self.x) + + # robot_future_st is optional here since you can use the same one from encoder_forward, + # but if it's given then we'll re-run that part of the model (if the node is adjacent to the robot). + def decoder_forward( + self, + prediction_horizon, + num_samples, + robot_present_and_future=None, + z_mode=False, + gmm_mode=False, + full_dist=False, + all_z_sep=False, + ): + # Always predicting with the online model. + mode = ModeKeys.PREDICT + + x_nr_t, y_r = None, None + if self.hyperparams["incl_robot_node"] and self.robot is not None and robot_present_and_future is not None: + our_inputs = torch.tensor( + self.node.get(np.array([self.node.last_timestep]), self.state[self.node.type], padding=0.0), + dtype=torch.float, + device=self.device, + ) + robot_present_and_future_st = get_relative_robot_traj( + self.env, self.state, our_inputs, robot_present_and_future, self.node.type, self.robot.type + ) + x_nr_t = robot_present_and_future_st[..., 0, :] + y_r = robot_present_and_future_st[..., 1:, :] + self.x = self.create_encoder_rep(mode, self.TD, x_nr_t, y_r) + self.latent.p_dist = self.p_z_x(mode, self.x) + + # Making sure n_s_t0 has the same batch size as x_nr_t + self.n_s_t0 = self.n_s_t0[[0]].repeat(x_nr_t.size()[0], 1) + + z, num_samples, num_components = self.latent.sample_p( + num_samples, mode, most_likely_z=z_mode, full_dist=full_dist, all_z_sep=all_z_sep + ) + + y_dist, our_sampled_future = self.p_y_xz( + mode, self.x, x_nr_t, y_r, self.n_s_t0, z, prediction_horizon, num_samples, num_components, gmm_mode + ) + + return y_dist, our_sampled_future diff --git a/forge/test/models/pytorch/multimodal/trajectron/trajectron/model/online/online_trajectron.py b/forge/test/models/pytorch/multimodal/trajectron/trajectron/model/online/online_trajectron.py new file mode 100644 index 000000000..73de7ed89 --- /dev/null +++ b/forge/test/models/pytorch/multimodal/trajectron/trajectron/model/online/online_trajectron.py @@ -0,0 +1,343 @@ +# SPDX-FileCopyrightText: (c) 2024 Tenstorrent AI ULC +# +# SPDX-License-Identifier: Apache-2.0 +import torch +import numpy as np +from collections import Counter +from model.trajectron import Trajectron +from model.online.online_mgcvae import OnlineMultimodalGenerativeCVAE +from model.model_utils import ModeKeys +from environment import RingBuffer, TemporalSceneGraph, SceneGraph, derivative_of + + +class OnlineTrajectron(Trajectron): + def __init__(self, model_registrar, hyperparams, device): + super(OnlineTrajectron, self).__init__( + model_registrar=model_registrar, hyperparams=hyperparams, log_writer=False, device=device + ) + self.node_data = dict() + self.scene_graph = None + self.RING_CAPACITY = ( + max( + len(self.hyperparams["edge_removal_filter"]), + len(self.hyperparams["edge_addition_filter"]), + self.hyperparams["maximum_history_length"], + ) + + 1 + ) + self.rel_states = dict() + self.removed_nodes = Counter() + + def __repr__(self): + return f"OnlineTrajectron(# nodes: {len(self.nodes)}, device: {self.device}, hyperparameters: {str(self.hyperparams)}) " + + def _add_node_model(self, node): + if node in self.nodes: + raise ValueError("%s was already added to this graph!" % str(node)) + + self.nodes.add(node) + self.node_models_dict[node] = OnlineMultimodalGenerativeCVAE( + self.env, node, self.model_registrar, self.hyperparams, self.device + ) + + def update_removed_nodes(self): + for node in list(self.removed_nodes.keys()): + if self.removed_nodes[node] >= len(self.hyperparams["edge_removal_filter"]): + del self.node_data[node] + del self.removed_nodes[node] + + def _remove_node_model(self, node): + if node not in self.nodes: + raise ValueError("%s is not in this graph!" % str(node)) + + self.nodes.remove(node) + del self.node_models_dict[node] + + def set_environment(self, env, init_timestep=0): + self.env = env + self.scene_graph = SceneGraph(edge_radius=self.env.attention_radius) + self.nodes.clear() + self.node_data.clear() + self.node_models_dict.clear() + + # Fast-forwarding ourselves to the initial timestep, without running any of the underlying models. + for timestep in range(init_timestep + 1): + self.incremental_forward( + self.env.scenes[0].get_clipped_input_dict(timestep, self.hyperparams["state"]), + maps=None, + run_models=False, + ) + + def incremental_forward( + self, + new_inputs_dict, + maps, + prediction_horizon=0, + num_samples=0, + robot_present_and_future=None, + z_mode=False, + gmm_mode=False, + full_dist=False, + all_z_sep=False, + run_models=True, + ): + # The way this function works is by appending the new datapoints to the + # ends of each of the LSTMs in the graph. Then, we recalculate the + # encoder's output vector h_x and feed that into the decoder to sample new outputs. + mode = ModeKeys.PREDICT + + # No grad since we're predicting always, as evidenced by the line above. + with torch.no_grad(): + for node, new_input in new_inputs_dict.items(): + if node not in self.node_data: + self.node_data[node] = RingBuffer( + capacity=self.RING_CAPACITY, + dtype=(float, sum(len(self.state[node.type][k]) for k in self.state[node.type])), + ) + self.node_data[node].append(new_input) + + if node in self.removed_nodes: + del self.removed_nodes[node] + + # Nodes in self.node_data that aren't in new_inputs_dict were just removed. + newly_removed_nodes = (set(self.node_data.keys()) - set(self.removed_nodes.keys())) - set( + new_inputs_dict.keys() + ) + + # We update self.removed_nodes with the newly removed nodes as well as all existing removed nodes to get + # the time since their last removal increased by one. + self.removed_nodes.update(newly_removed_nodes | set(self.removed_nodes.keys())) + + # For any nodes that are older than the length of the edge_removal_filter, we can safely clear their data. + self.update_removed_nodes() + + # Any remaining removed nodes that aren't yet old enough for data clearing simply have NaNs appended so + # that when it's passed through the LSTMs, the hidden state keeps propagating but the input plays no role + # (the NaNs get converted to zeros later on). + for node in self.removed_nodes: + self.node_data[node].append(np.full((1, self.node_data[node].shape[1]), np.nan)) + + for node in self.node_data: + node.overwrite_data( + self.node_data[node], + None, + forward_in_time_on_next_overwrite=(self.node_data[node].shape[0] == self.RING_CAPACITY), + ) + + temp_scene_dict = {k: v[:, 0:2] for k, v in self.node_data.items()} + if not temp_scene_dict: + new_scene_graph = SceneGraph(edge_radius=self.env.attention_radius) + else: + new_scene_graph = TemporalSceneGraph.create_from_temp_scene_dict( + temp_scene_dict, + self.env.attention_radius, + duration=self.RING_CAPACITY, + edge_addition_filter=self.hyperparams["edge_addition_filter"], + edge_removal_filter=self.hyperparams["edge_removal_filter"], + online=True, + ).to_scene_graph(t=self.RING_CAPACITY - 1) + + if self.hyperparams["dynamic_edges"] == "yes": + new_nodes, removed_nodes, new_neighbors, removed_neighbors = new_scene_graph - self.scene_graph + + # Aside from updating the scene graph, this for loop updates the graph model + # structure of all affected nodes. + not_removed_nodes = [node for node in self.nodes if node not in removed_nodes] + self.scene_graph = new_scene_graph + for node in not_removed_nodes: + self.node_models_dict[node].update_graph(new_scene_graph, new_neighbors, removed_neighbors) + + # These next 2 for loops add or remove entire node models. + for node in new_nodes: + if ( + node.is_robot and self.hyperparams["incl_robot_node"] + ) or node.type not in self.pred_state.keys(): + # Only deal with Models for NodeTypes we want to predict + continue + + self._add_node_model(node) + self.node_models_dict[node].update_graph(new_scene_graph, new_neighbors, removed_neighbors) + + for node in removed_nodes: + if ( + node.is_robot and self.hyperparams["incl_robot_node"] + ) or node.type not in self.pred_state.keys(): + continue + + self._remove_node_model(node) + + # This actually updates the node models with the newly observed data. + if run_models: + inputs = dict() + inputs_st = dict() + inputs_np = dict() + + iter_list = list(self.node_models_dict.keys()) + [ + node for node in new_inputs_dict if node.type not in self.pred_state.keys() + ] + if self.env.scenes[0].robot is not None: + iter_list.append(self.env.scenes[0].robot) + + for node in iter_list: + input_np = node.get(np.array([node.last_timestep, node.last_timestep]), self.state[node.type]) + + _, std = self.env.get_standardize_params(self.state[node.type.name], node.type) + std[0:2] = self.env.attention_radius[(node.type, node.type)] + rel_state = np.zeros_like(input_np) + rel_state[:, 0:2] = input_np[:, 0:2] + input_st = self.env.standardize(input_np, self.state[node.type.name], node.type, mean=rel_state) + self.rel_states[node] = rel_state + + # Converting NaNs to zeros. + input_np[np.isnan(input_np)] = 0 + input_st[np.isnan(input_st)] = 0 + + # Convert to torch tensors + inputs[node] = torch.tensor(input_np, dtype=torch.float, device=self.device) + inputs_st[node] = torch.tensor(input_st, dtype=torch.float, device=self.device) + inputs_np[node] = input_np + + # We want tensors of shape (1, ph + 1, state_dim) where the first 1 is the batch size. + if ( + self.hyperparams["incl_robot_node"] + and self.env.scenes[0].robot is not None + and robot_present_and_future is not None + ): + if len(robot_present_and_future.shape) == 2: + robot_present_and_future = robot_present_and_future[np.newaxis, :] + + assert robot_present_and_future.shape[1] == prediction_horizon + 1 + robot_present_and_future = torch.tensor( + robot_present_and_future, dtype=torch.float, device=self.device + ) + + for node in self.node_models_dict: + self.node_models_dict[node].encoder_forward( + inputs, inputs_st, inputs_np, robot_present_and_future, maps + ) + + # If num_predicted_timesteps or num_samples == 0 then do not run the decoder at all, + # just update the encoder LSTMs. + if prediction_horizon == 0 or num_samples == 0: + return + + return self.sample_model( + prediction_horizon, + num_samples, + robot_present_and_future=robot_present_and_future, + z_mode=z_mode, + gmm_mode=gmm_mode, + full_dist=full_dist, + all_z_sep=all_z_sep, + ) + + def _run_decoder( + self, + node, + num_predicted_timesteps, + num_samples, + robot_present_and_future=None, + z_mode=False, + gmm_mode=False, + full_dist=False, + all_z_sep=False, + ): + model = self.node_models_dict[node] + prediction_dist, predictions_uns = model.decoder_forward( + num_predicted_timesteps, + num_samples, + robot_present_and_future=robot_present_and_future, + z_mode=z_mode, + gmm_mode=gmm_mode, + full_dist=full_dist, + all_z_sep=all_z_sep, + ) + + predictions_np = predictions_uns.cpu().detach().numpy() + + # Return will be of shape (batch_size, num_samples, num_predicted_timesteps, 2) + return prediction_dist, np.transpose(predictions_np, (1, 0, 2, 3)) + + def sample_model( + self, + num_predicted_timesteps, + num_samples, + robot_present_and_future=None, + z_mode=False, + gmm_mode=False, + full_dist=False, + all_z_sep=False, + ): + # Just start from the encoder output (minus the + # robot future) and get num_samples of + # num_predicted_timesteps-length trajectories. + if num_predicted_timesteps == 0 or num_samples == 0: + return + + mode = ModeKeys.PREDICT + + # We want tensors of shape (1, ph + 1, state_dim) where the first 1 is the batch size. + if ( + self.hyperparams["incl_robot_node"] + and self.env.scenes[0].robot is not None + and robot_present_and_future is not None + ): + if len(robot_present_and_future.shape) == 2: + robot_present_and_future = robot_present_and_future[np.newaxis, :] + + assert robot_present_and_future.shape[1] == num_predicted_timesteps + 1 + + # No grad since we're predicting always, as evidenced by the line above. + with torch.no_grad(): + predictions_dict = dict() + prediction_dists = dict() + for node in set(self.nodes) - set(self.removed_nodes.keys()): + if node.is_robot: + continue + + prediction_dists[node], predictions_dict[node] = self._run_decoder( + node, + num_predicted_timesteps, + num_samples, + robot_present_and_future, + z_mode, + gmm_mode, + full_dist, + all_z_sep, + ) + + return prediction_dists, predictions_dict + + def forward( + self, + init_env, + init_timestep, + input_dicts, # After the initial environment + num_predicted_timesteps, + num_samples, + robot_present_and_future=None, + z_mode=False, + gmm_mode=False, + full_dist=False, + all_z_sep=False, + ): + # This is the standard forward prediction function, + # if you have some historical data and just want to + # predict forward some number of timesteps. + + # Setting us back to the initial scene graph we had. + self.set_environment(init_env, init_timestep) + + # Looping through and applying updates to the model. + for i in range(len(input_dicts)): + self.incremental_forward(input_dicts[i]) + + return self.sample_model( + num_predicted_timesteps, + num_samples, + robot_present_and_future=robot_present_and_future, + z_mode=z_mode, + gmm_mode=gmm_mode, + full_dist=full_dist, + all_z_sep=all_z_sep, + ) diff --git a/forge/test/models/pytorch/multimodal/trajectron/trajectron/model/trajectron.py b/forge/test/models/pytorch/multimodal/trajectron/trajectron/model/trajectron.py new file mode 100644 index 000000000..333a6b671 --- /dev/null +++ b/forge/test/models/pytorch/multimodal/trajectron/trajectron/model/trajectron.py @@ -0,0 +1,241 @@ +# SPDX-FileCopyrightText: (c) 2024 Tenstorrent AI ULC +# +# SPDX-License-Identifier: Apache-2.0 +import torch +import numpy as np +from model.mgcvae import MultimodalGenerativeCVAE +from model.dataset import get_timesteps_data, restore + + +class Trajectron(torch.nn.Module): + def __init__(self, model_registrar, hyperparams, log_writer, device): + super(Trajectron, self).__init__() + self.hyperparams = hyperparams + self.log_writer = log_writer + self.device = device + self.curr_iter = 0 + + self.model_registrar = model_registrar + # self.node_models_dict = dict() + self.node_models_dict = torch.nn.ModuleDict() + self.nodes = set() + + self.env = None + + self.min_ht = self.hyperparams["minimum_history_length"] + self.max_ht = self.hyperparams["maximum_history_length"] + self.ph = self.hyperparams["prediction_horizon"] + self.state = self.hyperparams["state"] + self.state_length = dict() + for state_type in self.state.keys(): + self.state_length[state_type] = int( + np.sum([len(entity_dims) for entity_dims in self.state[state_type].values()]) + ) + self.pred_state = self.hyperparams["pred_state"] + + def eval(self): + super().eval() + for key in self.node_models_dict.keys(): + self.node_models_dict[key].eval() + + def set_environment(self, env): + self.env = env + + self.node_models_dict.clear() + edge_types = env.get_edge_types() + + for node_type in env.NodeType: + # Only add a Model for NodeTypes we want to predict + if node_type in self.pred_state.keys(): + self.node_models_dict[str(node_type)] = MultimodalGenerativeCVAE( + env, + node_type, + self.model_registrar, + self.hyperparams, + self.device, + edge_types, + log_writer=self.log_writer, + ) + + def set_curr_iter(self, curr_iter): + self.curr_iter = curr_iter + for node_str, model in self.node_models_dict.items(): + model.set_curr_iter(curr_iter) + + def set_annealing_params(self): + for node_str, model in self.node_models_dict.items(): + model.set_annealing_params() + + def step_annealers(self, node_type=None): + if node_type is None: + for node_type in self.node_models_dict: + self.node_models_dict[node_type].step_annealers() + else: + self.node_models_dict[node_type].step_annealers() + + def train_loss(self, batch, node_type): + ( + first_history_index, + x_t, + y_t, + x_st_t, + y_st_t, + neighbors_data_st, + neighbors_edge_value, + robot_traj_st_t, + map, + ) = batch + + x = x_t.to(self.device) + y = y_t.to(self.device) + x_st_t = x_st_t.to(self.device) + y_st_t = y_st_t.to(self.device) + if robot_traj_st_t is not None: + robot_traj_st_t = robot_traj_st_t.to(self.device) + if type(map) == torch.Tensor: + map = map.to(self.device) + + # Run forward pass + model = self.node_models_dict[node_type] + loss = model.train_loss( + inputs=x, + inputs_st=x_st_t, + first_history_indices=first_history_index, + labels=y, + labels_st=y_st_t, + neighbors=restore(neighbors_data_st), + neighbors_edge_value=restore(neighbors_edge_value), + robot=robot_traj_st_t, + map=map, + prediction_horizon=self.ph, + ) + + return loss + + def eval_loss(self, batch, node_type): + ( + first_history_index, + x_t, + y_t, + x_st_t, + y_st_t, + neighbors_data_st, + neighbors_edge_value, + robot_traj_st_t, + map, + ) = batch + + x = x_t.to(self.device) + y = y_t.to(self.device) + x_st_t = x_st_t.to(self.device) + y_st_t = y_st_t.to(self.device) + if robot_traj_st_t is not None: + robot_traj_st_t = robot_traj_st_t.to(self.device) + if type(map) == torch.Tensor: + map = map.to(self.device) + + # Run forward pass + model = self.node_models_dict[node_type] + nll = model.eval_loss( + inputs=x, + inputs_st=x_st_t, + first_history_indices=first_history_index, + labels=y, + labels_st=y_st_t, + neighbors=restore(neighbors_data_st), + neighbors_edge_value=restore(neighbors_edge_value), + robot=robot_traj_st_t, + map=map, + prediction_horizon=self.ph, + ) + + return nll.cpu().detach().numpy() + + def predict( + self, + scene, + timesteps, + ph, + num_samples=1, + min_future_timesteps=0, + min_history_timesteps=1, + z_mode=False, + gmm_mode=False, + full_dist=True, + all_z_sep=False, + ): + + predictions_dict = {} + for node_type in self.env.NodeType: + if node_type not in self.pred_state: + continue + + model = self.node_models_dict[node_type] + + # Get Input data for node type and given timesteps + batch = get_timesteps_data( + env=self.env, + scene=scene, + t=timesteps, + node_type=node_type, + state=self.state, + pred_state=self.pred_state, + edge_types=model.edge_types, + min_ht=min_history_timesteps, + max_ht=self.max_ht, + min_ft=min_future_timesteps, + max_ft=min_future_timesteps, + hyperparams=self.hyperparams, + ) + # There are no nodes of type present for timestep + if batch is None: + continue + ( + ( + first_history_index, + x_t, + y_t, + x_st_t, + y_st_t, + neighbors_data_st, + neighbors_edge_value, + robot_traj_st_t, + map, + ), + nodes, + timesteps_o, + ) = batch + + x = x_t.to(self.device) + x_st_t = x_st_t.to(self.device) + if robot_traj_st_t is not None: + robot_traj_st_t = robot_traj_st_t.to(self.device) + if type(map) == torch.Tensor: + map = map.to(self.device) + + # Run forward pass + predictions = model.predict( + inputs=x, + inputs_st=x_st_t, + first_history_indices=first_history_index, + neighbors=neighbors_data_st, + neighbors_edge_value=neighbors_edge_value, + robot=robot_traj_st_t, + map=map, + prediction_horizon=ph, + num_samples=num_samples, + z_mode=z_mode, + gmm_mode=gmm_mode, + full_dist=full_dist, + all_z_sep=all_z_sep, + ) + + predictions_np = predictions.cpu().detach().numpy() + + # Assign predictions to node + for i, ts in enumerate(timesteps_o): + if ts not in predictions_dict.keys(): + predictions_dict[ts] = dict() + predictions_dict[ts][nodes[i]] = np.transpose(predictions_np[:, [i]], (1, 0, 2, 3)) + + return predictions_dict diff --git a/forge/test/models/pytorch/multimodal/trajectron/trajectron/model_dir/config.json b/forge/test/models/pytorch/multimodal/trajectron/trajectron/model_dir/config.json new file mode 100644 index 000000000..bf417f081 --- /dev/null +++ b/forge/test/models/pytorch/multimodal/trajectron/trajectron/model_dir/config.json @@ -0,0 +1 @@ +{"batch_size": 256, "grad_clip": 1.0, "learning_rate_style": "exp", "learning_rate": 0.001, "min_learning_rate": 1e-05, "learning_decay_rate": 0.9999, "prediction_horizon": 12, "minimum_history_length": 1, "maximum_history_length": 8, "map_encoder": {"PEDESTRIAN": {"heading_state_index": 5, "patch_size": [50, 10, 50, 90], "map_channels": 3, "hidden_channels": [10, 20, 10, 1], "output_size": 32, "masks": [5, 5, 5, 5], "strides": [1, 1, 1, 1], "dropout": 0.5}}, "k": 1, "k_eval": 1, "kl_min": 0.07, "kl_weight": 100.0, "kl_weight_start": 0, "kl_decay_rate": 0.99995, "kl_crossover": 400, "kl_sigmoid_divisor": 4, "rnn_kwargs": {"dropout_keep_prob": 0.75}, "MLP_dropout_keep_prob": 0.9, "enc_rnn_dim_edge": 32, "enc_rnn_dim_edge_influence": 32, "enc_rnn_dim_history": 32, "enc_rnn_dim_future": 32, "dec_rnn_dim": 128, "q_z_xy_MLP_dims": null, "p_z_x_MLP_dims": 32, "GMM_components": 1, "log_p_yt_xz_max": 6, "N": 1, "K": 25, "tau_init": 2.0, "tau_final": 0.05, "tau_decay_rate": 0.997, "use_z_logit_clipping": true, "z_logit_clip_start": 0.05, "z_logit_clip_final": 5.0, "z_logit_clip_crossover": 300, "z_logit_clip_divisor": 5, "dynamic": {"PEDESTRIAN": {"name": "SingleIntegrator", "distribution": true, "limits": {}}}, "state": {"PEDESTRIAN": {"position": ["x", "y"], "velocity": ["x", "y"], "acceleration": ["x", "y"]}}, "pred_state": {"PEDESTRIAN": {"position": ["x", "y"]}}, "log_histograms": false, "scene_freq_mult_eval": false, "node_freq_mult_eval": false, "edge_encoding": false, "incl_robot_node": false, "use_map_encoding": false} diff --git a/forge/test/models/pytorch/multimodal/trajectron/trajectron/test/__init__.py b/forge/test/models/pytorch/multimodal/trajectron/trajectron/test/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/forge/test/models/pytorch/multimodal/trajectron/trajectron/test/test_data_structures.py b/forge/test/models/pytorch/multimodal/trajectron/trajectron/test/test_data_structures.py new file mode 100644 index 000000000..e840fda99 --- /dev/null +++ b/forge/test/models/pytorch/multimodal/trajectron/trajectron/test/test_data_structures.py @@ -0,0 +1,46 @@ +# SPDX-FileCopyrightText: (c) 2024 Tenstorrent AI ULC +# +# SPDX-License-Identifier: Apache-2.0 +import numpy as np +import pandas as pd +from data import SingleHeaderNumpyArray, DoubleHeaderNumpyArray + + +def test_single_header_numpy_array(): + x = np.random.rand(10) + y = np.random.rand(10) + + array = SingleHeaderNumpyArray(np.stack((x, y), axis=-1), ["x", "y"]) + + assert (array[:, "x"] == x).all() + assert (array[:, "y"] == y).all() + assert (array[3:7, "y"] == y[3:7]).all() + assert (array.x == x).all() + assert (array.y == y).all() + + +def test_double_header_numpy_array(): + x = np.random.rand(10) + y = np.random.rand(10) + vx = np.random.rand(10) + vy = np.random.rand(10) + + data_dict = {("position", "x"): x, ("position", "y"): y, ("velocity", "x"): vx, ("velocity", "y"): vy} + + data_columns = pd.MultiIndex.from_product([["position", "velocity"], ["x", "y"]]) + + node_data = pd.DataFrame(data_dict, columns=data_columns) + + array = DoubleHeaderNumpyArray(node_data.values, list(node_data.columns)) + + test_header_dict = {"position": ["x", "y"], "velocity": ["y"]} + + assert (array[:, ("position", "x")] == x).all() + assert (array[:, ("velocity", "y")] == vy).all() + assert (array[4:7, ("velocity", "y")] == vy[4:7]).all() + assert (array[:, [("position", "x"), ("velocity", "y")]] == np.stack((x, vy), axis=-1)).all() + assert (array[:, [("position", "y"), ("velocity", "x")]] == np.stack((y, vx), axis=-1)).all() + assert (array[2:6, [("position", "y"), ("velocity", "x")]] == np.stack((y, vx), axis=-1)[2:6]).all() + assert (array[:, test_header_dict] == np.stack((x, y, vy), axis=-1)).all() + assert (array[1:8, test_header_dict] == np.stack((x, y, vy), axis=-1)[1:8]).all() + assert (array.position.x == x).all() diff --git a/forge/test/models/pytorch/multimodal/trajectron/trajectron/test_online.py b/forge/test/models/pytorch/multimodal/trajectron/trajectron/test_online.py new file mode 100644 index 000000000..123b8e87e --- /dev/null +++ b/forge/test/models/pytorch/multimodal/trajectron/trajectron/test_online.py @@ -0,0 +1,252 @@ +# SPDX-FileCopyrightText: (c) 2024 Tenstorrent AI ULC +# +# SPDX-License-Identifier: Apache-2.0 +import os +import time +import json +import torch +import dill +import random +import pathlib +import evaluation +import numpy as np +import visualization as vis +from argument_parser import args +from model.online.online_trajectron import OnlineTrajectron +from model.model_registrar import ModelRegistrar +from environment import Environment, Scene +import matplotlib.pyplot as plt + +if not torch.cuda.is_available() or args.device == "cpu": + args.device = torch.device("cpu") +else: + if torch.cuda.device_count() == 1: + # If you have CUDA_VISIBLE_DEVICES set, which you should, + # then this will prevent leftover flag arguments from + # messing with the device allocation. + args.device = "cuda:0" + + args.device = torch.device(args.device) + +if args.eval_device is None: + args.eval_device = "cpu" + +if args.seed is not None: + random.seed(args.seed) + np.random.seed(args.seed) + torch.manual_seed(args.seed) + if torch.cuda.is_available(): + torch.cuda.manual_seed_all(args.seed) + + +def create_online_env(env, hyperparams, scene_idx, init_timestep): + test_scene = env.scenes[scene_idx] + + online_scene = Scene(timesteps=init_timestep + 1, map=test_scene.map, dt=test_scene.dt) + online_scene.nodes = test_scene.get_nodes_clipped_at_time( + timesteps=np.arange(init_timestep - hyperparams["maximum_history_length"], init_timestep + 1), + state=hyperparams["state"], + ) + online_scene.robot = test_scene.robot + online_scene.calculate_scene_graph( + attention_radius=env.attention_radius, + edge_addition_filter=hyperparams["edge_addition_filter"], + edge_removal_filter=hyperparams["edge_removal_filter"], + ) + + return Environment( + node_type_list=env.node_type_list, + standardization=env.standardization, + scenes=[online_scene], + attention_radius=env.attention_radius, + robot_type=env.robot_type, + ) + + +def get_maps_for_input(input_dict, scene, hyperparams): + scene_maps = list() + scene_pts = list() + heading_angles = list() + patch_sizes = list() + nodes_with_maps = list() + for node in input_dict: + if node.type in hyperparams["map_encoder"]: + x = input_dict[node] + me_hyp = hyperparams["map_encoder"][node.type] + if "heading_state_index" in me_hyp: + heading_state_index = me_hyp["heading_state_index"] + # We have to rotate the map in the opposit direction of the agent to match them + if type(heading_state_index) is list: # infer from velocity or heading vector + heading_angle = ( + -np.arctan2(x[-1, heading_state_index[1]], x[-1, heading_state_index[0]]) * 180 / np.pi + ) + else: + heading_angle = -x[-1, heading_state_index] * 180 / np.pi + else: + heading_angle = None + + scene_map = scene.map[node.type] + map_point = x[-1, :2] + + patch_size = hyperparams["map_encoder"][node.type]["patch_size"] + + scene_maps.append(scene_map) + scene_pts.append(map_point) + heading_angles.append(heading_angle) + patch_sizes.append(patch_size) + nodes_with_maps.append(node) + + if heading_angles[0] is None: + heading_angles = None + else: + heading_angles = torch.Tensor(heading_angles) + + maps = scene_maps[0].get_cropped_maps_from_scene_map_batch( + scene_maps, scene_pts=torch.Tensor(scene_pts), patch_size=patch_sizes[0], rotation=heading_angles + ) + + maps_dict = {node: maps[[i]] for i, node in enumerate(nodes_with_maps)} + return maps_dict + + +def main(): + # Choose one of the model directory names under the experiment/*/models folders. + # Possibilities are 'vel_ee', 'int_ee', 'int_ee_me', or 'robot' + model_dir = os.path.join(args.log_dir, "int_ee") + + # Load hyperparameters from json + config_file = os.path.join(model_dir, args.conf) + if not os.path.exists(config_file): + raise ValueError("Config json not found!") + with open(config_file, "r") as conf_json: + hyperparams = json.load(conf_json) + + # Add hyperparams from arguments + hyperparams["dynamic_edges"] = args.dynamic_edges + hyperparams["edge_state_combine_method"] = args.edge_state_combine_method + hyperparams["edge_influence_combine_method"] = args.edge_influence_combine_method + hyperparams["edge_addition_filter"] = args.edge_addition_filter + hyperparams["edge_removal_filter"] = args.edge_removal_filter + hyperparams["batch_size"] = args.batch_size + hyperparams["k_eval"] = args.k_eval + hyperparams["offline_scene_graph"] = args.offline_scene_graph + hyperparams["incl_robot_node"] = args.incl_robot_node + hyperparams["edge_encoding"] = not args.no_edge_encoding + hyperparams["use_map_encoding"] = args.map_encoding + + output_save_dir = os.path.join(model_dir, "pred_figs") + pathlib.Path(output_save_dir).mkdir(parents=True, exist_ok=True) + + eval_data_path = os.path.join(args.data_dir, args.eval_data_dict) + with open(eval_data_path, "rb") as f: + eval_env = dill.load(f, encoding="latin1") + + if eval_env.robot_type is None and hyperparams["incl_robot_node"]: + eval_env.robot_type = eval_env.NodeType[0] # TODO: Make more general, allow the user to specify? + for scene in eval_env.scenes: + scene.add_robot_from_nodes(eval_env.robot_type) + + print("Loaded data from %s" % (eval_data_path,)) + + # Creating a dummy environment with a single scene that contains information about the world. + # When using this code, feel free to use whichever scene index or initial timestep you wish. + scene_idx = 0 + + # You need to have at least acceleration, so you want 2 timesteps of prior data, e.g. [0, 1], + # so that you can immediately start incremental inference from the 3rd timestep onwards. + init_timestep = 1 + + eval_scene = eval_env.scenes[scene_idx] + online_env = create_online_env(eval_env, hyperparams, scene_idx, init_timestep) + + model_registrar = ModelRegistrar(model_dir, args.eval_device) + model_registrar.load_models(iter_num=12) + + trajectron = OnlineTrajectron(model_registrar, hyperparams, args.eval_device) + + # If you want to see what different robot futures do to the predictions, uncomment this line as well as + # related "... += adjustment" lines below. + # adjustment = np.stack([np.arange(13)/float(i*2.0) for i in range(6, 12)], axis=1) + + # Here's how you'd incrementally run the model, e.g. with streaming data. + trajectron.set_environment(online_env, init_timestep) + + for timestep in range(init_timestep + 1, eval_scene.timesteps): + input_dict = eval_scene.get_clipped_input_dict(timestep, hyperparams["state"]) + + maps = None + if hyperparams["use_map_encoding"]: + maps = get_maps_for_input(input_dict, eval_scene, hyperparams) + + robot_present_and_future = None + if eval_scene.robot is not None and hyperparams["incl_robot_node"]: + robot_present_and_future = eval_scene.robot.get( + np.array([timestep, timestep + hyperparams["prediction_horizon"]]), + hyperparams["state"][eval_scene.robot.type], + padding=0.0, + ) + robot_present_and_future = np.stack([robot_present_and_future, robot_present_and_future], axis=0) + # robot_present_and_future += adjustment + + start = time.time() + dists, preds = trajectron.incremental_forward( + input_dict, + maps, + prediction_horizon=6, + num_samples=1, + robot_present_and_future=robot_present_and_future, + full_dist=True, + ) + end = time.time() + print( + "t=%d: took %.2f s (= %.2f Hz) w/ %d nodes and %d edges" + % ( + timestep, + end - start, + 1.0 / (end - start), + len(trajectron.nodes), + trajectron.scene_graph.get_num_edges(), + ) + ) + + detailed_preds_dict = dict() + for node in eval_scene.nodes: + if node in preds: + detailed_preds_dict[node] = preds[node] + + fig, ax = plt.subplots() + vis.visualize_distribution(ax, dists) + vis.visualize_prediction( + ax, + {timestep: preds}, + eval_scene.dt, + hyperparams["maximum_history_length"], + hyperparams["prediction_horizon"], + ) + + if eval_scene.robot is not None and hyperparams["incl_robot_node"]: + robot_for_plotting = eval_scene.robot.get( + np.array([timestep, timestep + hyperparams["prediction_horizon"]]), + hyperparams["state"][eval_scene.robot.type], + ) + # robot_for_plotting += adjustment + + ax.plot(robot_for_plotting[1:, 1], robot_for_plotting[1:, 0], color="r", linewidth=1.0, alpha=1.0) + + # Current Node Position + circle = plt.Circle( + (robot_for_plotting[0, 1], robot_for_plotting[0, 0]), + 0.3, + facecolor="r", + edgecolor="k", + lw=0.5, + zorder=3, + ) + ax.add_artist(circle) + + fig.savefig(os.path.join(output_save_dir, f"pred_{timestep}.pdf"), dpi=300) + plt.close(fig) + + +if __name__ == "__main__": + main() diff --git a/forge/test/models/pytorch/multimodal/trajectron/trajectron/train.py b/forge/test/models/pytorch/multimodal/trajectron/trajectron/train.py new file mode 100644 index 000000000..274e3e0de --- /dev/null +++ b/forge/test/models/pytorch/multimodal/trajectron/trajectron/train.py @@ -0,0 +1,452 @@ +# SPDX-FileCopyrightText: (c) 2024 Tenstorrent AI ULC +# +# SPDX-License-Identifier: Apache-2.0 +import torch +from torch import nn, optim, utils +import numpy as np +import os +import time +import dill +import json +import random +import pathlib +import warnings +from tqdm import tqdm +import visualization +import evaluation +import matplotlib.pyplot as plt +from argument_parser import args +from model.trajectron import Trajectron +from model.model_registrar import ModelRegistrar +from model.model_utils import cyclical_lr +from model.dataset import EnvironmentDataset, collate +from tensorboardX import SummaryWriter + +# torch.autograd.set_detect_anomaly(True) + +if not torch.cuda.is_available() or args.device == "cpu": + args.device = torch.device("cpu") +else: + if torch.cuda.device_count() == 1: + # If you have CUDA_VISIBLE_DEVICES set, which you should, + # then this will prevent leftover flag arguments from + # messing with the device allocation. + args.device = "cuda:0" + + args.device = torch.device(args.device) + +if args.eval_device is None: + args.eval_device = torch.device("cpu") + +# This is needed for memory pinning using a DataLoader (otherwise memory is pinned to cuda:0 by default) +torch.cuda.set_device(args.device) + +if args.seed is not None: + random.seed(args.seed) + np.random.seed(args.seed) + torch.manual_seed(args.seed) + if torch.cuda.is_available(): + torch.cuda.manual_seed_all(args.seed) + + +def main(): + # Load hyperparameters from json + if not os.path.exists(args.conf): + print("Config json not found!") + with open(args.conf, "r", encoding="utf-8") as conf_json: + hyperparams = json.load(conf_json) + + # Add hyperparams from arguments + hyperparams["dynamic_edges"] = args.dynamic_edges + hyperparams["edge_state_combine_method"] = args.edge_state_combine_method + hyperparams["edge_influence_combine_method"] = args.edge_influence_combine_method + hyperparams["edge_addition_filter"] = args.edge_addition_filter + hyperparams["edge_removal_filter"] = args.edge_removal_filter + hyperparams["batch_size"] = args.batch_size + hyperparams["k_eval"] = args.k_eval + hyperparams["offline_scene_graph"] = args.offline_scene_graph + hyperparams["incl_robot_node"] = args.incl_robot_node + hyperparams["node_freq_mult_train"] = args.node_freq_mult_train + hyperparams["node_freq_mult_eval"] = args.node_freq_mult_eval + hyperparams["scene_freq_mult_train"] = args.scene_freq_mult_train + hyperparams["scene_freq_mult_eval"] = args.scene_freq_mult_eval + hyperparams["scene_freq_mult_viz"] = args.scene_freq_mult_viz + hyperparams["edge_encoding"] = not args.no_edge_encoding + hyperparams["use_map_encoding"] = args.map_encoding + hyperparams["augment"] = args.augment + hyperparams["override_attention_radius"] = args.override_attention_radius + + print("-----------------------") + print("| TRAINING PARAMETERS |") + print("-----------------------") + print("| batch_size: %d" % args.batch_size) + print("| device: %s" % args.device) + print("| eval_device: %s" % args.eval_device) + print("| Offline Scene Graph Calculation: %s" % args.offline_scene_graph) + print("| EE state_combine_method: %s" % args.edge_state_combine_method) + print("| EIE scheme: %s" % args.edge_influence_combine_method) + print("| dynamic_edges: %s" % args.dynamic_edges) + print("| robot node: %s" % args.incl_robot_node) + print("| edge_addition_filter: %s" % args.edge_addition_filter) + print("| edge_removal_filter: %s" % args.edge_removal_filter) + print("| MHL: %s" % hyperparams["minimum_history_length"]) + print("| PH: %s" % hyperparams["prediction_horizon"]) + print("-----------------------") + + log_writer = None + model_dir = None + if not args.debug: + # Create the log and model directiory if they're not present. + model_dir = os.path.join( + args.log_dir, "models_" + time.strftime("%d_%b_%Y_%H_%M_%S", time.localtime()) + args.log_tag + ) + pathlib.Path(model_dir).mkdir(parents=True, exist_ok=True) + + # Save config to model directory + with open(os.path.join(model_dir, "config.json"), "w") as conf_json: + json.dump(hyperparams, conf_json) + + log_writer = SummaryWriter(log_dir=model_dir) + + # Load training and evaluation environments and scenes + train_scenes = [] + train_data_path = os.path.join(args.data_dir, args.train_data_dict) + with open(train_data_path, "rb") as f: + train_env = dill.load(f, encoding="latin1") + + for attention_radius_override in args.override_attention_radius: + node_type1, node_type2, attention_radius = attention_radius_override.split(" ") + train_env.attention_radius[(node_type1, node_type2)] = float(attention_radius) + + if train_env.robot_type is None and hyperparams["incl_robot_node"]: + train_env.robot_type = train_env.NodeType[0] # TODO: Make more general, allow the user to specify? + for scene in train_env.scenes: + scene.add_robot_from_nodes(train_env.robot_type) + + train_scenes = train_env.scenes + train_scenes_sample_probs = train_env.scenes_freq_mult_prop if args.scene_freq_mult_train else None + + train_dataset = EnvironmentDataset( + train_env, + hyperparams["state"], + hyperparams["pred_state"], + scene_freq_mult=hyperparams["scene_freq_mult_train"], + node_freq_mult=hyperparams["node_freq_mult_train"], + hyperparams=hyperparams, + min_history_timesteps=hyperparams["minimum_history_length"], + min_future_timesteps=hyperparams["prediction_horizon"], + return_robot=not args.incl_robot_node, + ) + train_data_loader = dict() + for node_type_data_set in train_dataset: + if len(node_type_data_set) == 0: + continue + + node_type_dataloader = utils.data.DataLoader( + node_type_data_set, + collate_fn=collate, + pin_memory=False if args.device is "cpu" else True, + batch_size=args.batch_size, + shuffle=True, + num_workers=args.preprocess_workers, + ) + train_data_loader[node_type_data_set.node_type] = node_type_dataloader + + print(f"Loaded training data from {train_data_path}") + + eval_scenes = [] + eval_scenes_sample_probs = None + if args.eval_every is not None: + eval_data_path = os.path.join(args.data_dir, args.eval_data_dict) + with open(eval_data_path, "rb") as f: + eval_env = dill.load(f, encoding="latin1") + + for attention_radius_override in args.override_attention_radius: + node_type1, node_type2, attention_radius = attention_radius_override.split(" ") + eval_env.attention_radius[(node_type1, node_type2)] = float(attention_radius) + + if eval_env.robot_type is None and hyperparams["incl_robot_node"]: + eval_env.robot_type = eval_env.NodeType[0] # TODO: Make more general, allow the user to specify? + for scene in eval_env.scenes: + scene.add_robot_from_nodes(eval_env.robot_type) + + eval_scenes = eval_env.scenes + eval_scenes_sample_probs = eval_env.scenes_freq_mult_prop if args.scene_freq_mult_eval else None + + eval_dataset = EnvironmentDataset( + eval_env, + hyperparams["state"], + hyperparams["pred_state"], + scene_freq_mult=hyperparams["scene_freq_mult_eval"], + node_freq_mult=hyperparams["node_freq_mult_eval"], + hyperparams=hyperparams, + min_history_timesteps=hyperparams["minimum_history_length"], + min_future_timesteps=hyperparams["prediction_horizon"], + return_robot=not args.incl_robot_node, + ) + eval_data_loader = dict() + for node_type_data_set in eval_dataset: + if len(node_type_data_set) == 0: + continue + + node_type_dataloader = utils.data.DataLoader( + node_type_data_set, + collate_fn=collate, + pin_memory=False if args.eval_device is "cpu" else True, + batch_size=args.eval_batch_size, + shuffle=True, + num_workers=args.preprocess_workers, + ) + eval_data_loader[node_type_data_set.node_type] = node_type_dataloader + + print(f"Loaded evaluation data from {eval_data_path}") + + # Offline Calculate Scene Graph + if hyperparams["offline_scene_graph"] == "yes": + print(f"Offline calculating scene graphs") + for i, scene in enumerate(train_scenes): + scene.calculate_scene_graph( + train_env.attention_radius, hyperparams["edge_addition_filter"], hyperparams["edge_removal_filter"] + ) + print(f"Created Scene Graph for Training Scene {i}") + + for i, scene in enumerate(eval_scenes): + scene.calculate_scene_graph( + eval_env.attention_radius, hyperparams["edge_addition_filter"], hyperparams["edge_removal_filter"] + ) + print(f"Created Scene Graph for Evaluation Scene {i}") + + model_registrar = ModelRegistrar(model_dir, args.device) + + trajectron = Trajectron(model_registrar, hyperparams, log_writer, args.device) + + trajectron.set_environment(train_env) + trajectron.set_annealing_params() + print("Created Training Model.") + + eval_trajectron = None + if args.eval_every is not None or args.vis_every is not None: + eval_trajectron = Trajectron(model_registrar, hyperparams, log_writer, args.eval_device) + eval_trajectron.set_environment(eval_env) + eval_trajectron.set_annealing_params() + print("Created Evaluation Model.") + + optimizer = dict() + lr_scheduler = dict() + for node_type in train_env.NodeType: + if node_type not in hyperparams["pred_state"]: + continue + optimizer[node_type] = optim.Adam( + [ + {"params": model_registrar.get_all_but_name_match("map_encoder").parameters()}, + {"params": model_registrar.get_name_match("map_encoder").parameters(), "lr": 0.0008}, + ], + lr=hyperparams["learning_rate"], + ) + # Set Learning Rate + if hyperparams["learning_rate_style"] == "const": + lr_scheduler[node_type] = optim.lr_scheduler.ExponentialLR(optimizer[node_type], gamma=1.0) + elif hyperparams["learning_rate_style"] == "exp": + lr_scheduler[node_type] = optim.lr_scheduler.ExponentialLR( + optimizer[node_type], gamma=hyperparams["learning_decay_rate"] + ) + + ################################# + # TRAINING # + ################################# + curr_iter_node_type = {node_type: 0 for node_type in train_data_loader.keys()} + for epoch in range(1, args.train_epochs + 1): + model_registrar.to(args.device) + train_dataset.augment = args.augment + for node_type, data_loader in train_data_loader.items(): + curr_iter = curr_iter_node_type[node_type] + pbar = tqdm(data_loader, ncols=80) + for batch in pbar: + trajectron.set_curr_iter(curr_iter) + trajectron.step_annealers(node_type) + optimizer[node_type].zero_grad() + train_loss = trajectron.train_loss(batch, node_type) + pbar.set_description(f"Epoch {epoch}, {node_type} L: {train_loss.item():.2f}") + train_loss.backward() + # Clipping gradients. + if hyperparams["grad_clip"] is not None: + nn.utils.clip_grad_value_(model_registrar.parameters(), hyperparams["grad_clip"]) + optimizer[node_type].step() + + # Stepping forward the learning rate scheduler and annealers. + lr_scheduler[node_type].step() + + if not args.debug: + log_writer.add_scalar( + f"{node_type}/train/learning_rate", lr_scheduler[node_type].get_lr()[0], curr_iter + ) + log_writer.add_scalar(f"{node_type}/train/loss", train_loss, curr_iter) + + curr_iter += 1 + curr_iter_node_type[node_type] = curr_iter + train_dataset.augment = False + if args.eval_every is not None or args.vis_every is not None: + eval_trajectron.set_curr_iter(epoch) + + ################################# + # VISUALIZATION # + ################################# + if args.vis_every is not None and not args.debug and epoch % args.vis_every == 0 and epoch > 0: + max_hl = hyperparams["maximum_history_length"] + ph = hyperparams["prediction_horizon"] + with torch.no_grad(): + # Predict random timestep to plot for train data set + if args.scene_freq_mult_viz: + scene = np.random.choice(train_scenes, p=train_scenes_sample_probs) + else: + scene = np.random.choice(train_scenes) + timestep = scene.sample_timesteps(1, min_future_timesteps=ph) + predictions = trajectron.predict( + scene, + timestep, + ph, + min_future_timesteps=ph, + z_mode=True, + gmm_mode=True, + all_z_sep=False, + full_dist=False, + ) + + # Plot predicted timestep for random scene + fig, ax = plt.subplots(figsize=(10, 10)) + visualization.visualize_prediction( + ax, + predictions, + scene.dt, + max_hl=max_hl, + ph=ph, + map=scene.map["VISUALIZATION"] if scene.map is not None else None, + ) + ax.set_title(f"{scene.name}-t: {timestep}") + log_writer.add_figure("train/prediction", fig, epoch) + + model_registrar.to(args.eval_device) + # Predict random timestep to plot for eval data set + if args.scene_freq_mult_viz: + scene = np.random.choice(eval_scenes, p=eval_scenes_sample_probs) + else: + scene = np.random.choice(eval_scenes) + timestep = scene.sample_timesteps(1, min_future_timesteps=ph) + predictions = eval_trajectron.predict( + scene, timestep, ph, num_samples=20, min_future_timesteps=ph, z_mode=False, full_dist=False + ) + + # Plot predicted timestep for random scene + fig, ax = plt.subplots(figsize=(10, 10)) + visualization.visualize_prediction( + ax, + predictions, + scene.dt, + max_hl=max_hl, + ph=ph, + map=scene.map["VISUALIZATION"] if scene.map is not None else None, + ) + ax.set_title(f"{scene.name}-t: {timestep}") + log_writer.add_figure("eval/prediction", fig, epoch) + + # Predict random timestep to plot for eval data set + predictions = eval_trajectron.predict( + scene, + timestep, + ph, + min_future_timesteps=ph, + z_mode=True, + gmm_mode=True, + all_z_sep=True, + full_dist=False, + ) + + # Plot predicted timestep for random scene + fig, ax = plt.subplots(figsize=(10, 10)) + visualization.visualize_prediction( + ax, + predictions, + scene.dt, + max_hl=max_hl, + ph=ph, + map=scene.map["VISUALIZATION"] if scene.map is not None else None, + ) + ax.set_title(f"{scene.name}-t: {timestep}") + log_writer.add_figure("eval/prediction_all_z", fig, epoch) + + ################################# + # EVALUATION # + ################################# + if args.eval_every is not None and not args.debug and epoch % args.eval_every == 0 and epoch > 0: + max_hl = hyperparams["maximum_history_length"] + ph = hyperparams["prediction_horizon"] + model_registrar.to(args.eval_device) + with torch.no_grad(): + # Calculate evaluation loss + for node_type, data_loader in eval_data_loader.items(): + eval_loss = [] + print(f"Starting Evaluation @ epoch {epoch} for node type: {node_type}") + pbar = tqdm(data_loader, ncols=80) + for batch in pbar: + eval_loss_node_type = eval_trajectron.eval_loss(batch, node_type) + pbar.set_description(f"Epoch {epoch}, {node_type} L: {eval_loss_node_type.item():.2f}") + eval_loss.append({node_type: {"nll": [eval_loss_node_type]}}) + del batch + + evaluation.log_batch_errors(eval_loss, log_writer, f"{node_type}/eval_loss", epoch) + + # Predict batch timesteps for evaluation dataset evaluation + eval_batch_errors = [] + for scene in tqdm(eval_scenes, desc="Sample Evaluation", ncols=80): + timesteps = scene.sample_timesteps(args.eval_batch_size) + + predictions = eval_trajectron.predict( + scene, timesteps, ph, num_samples=50, min_future_timesteps=ph, full_dist=False + ) + + eval_batch_errors.append( + evaluation.compute_batch_statistics( + predictions, scene.dt, max_hl=max_hl, ph=ph, node_type_enum=eval_env.NodeType, map=scene.map + ) + ) + + evaluation.log_batch_errors( + eval_batch_errors, log_writer, "eval", epoch, bar_plot=["kde"], box_plot=["ade", "fde"] + ) + + # Predict maximum likelihood batch timesteps for evaluation dataset evaluation + eval_batch_errors_ml = [] + for scene in tqdm(eval_scenes, desc="MM Evaluation", ncols=80): + timesteps = scene.sample_timesteps(scene.timesteps) + + predictions = eval_trajectron.predict( + scene, + timesteps, + ph, + num_samples=1, + min_future_timesteps=ph, + z_mode=True, + gmm_mode=True, + full_dist=False, + ) + + eval_batch_errors_ml.append( + evaluation.compute_batch_statistics( + predictions, + scene.dt, + max_hl=max_hl, + ph=ph, + map=scene.map, + node_type_enum=eval_env.NodeType, + kde=False, + ) + ) + + evaluation.log_batch_errors(eval_batch_errors_ml, log_writer, "eval/ml", epoch) + + if args.save_every is not None and args.debug is False and epoch % args.save_every == 0: + model_registrar.save_models(epoch) + + +if __name__ == "__main__": + main() diff --git a/forge/test/models/pytorch/multimodal/trajectron/trajectron/utils/__init__.py b/forge/test/models/pytorch/multimodal/trajectron/trajectron/utils/__init__.py new file mode 100644 index 000000000..9200d8dee --- /dev/null +++ b/forge/test/models/pytorch/multimodal/trajectron/trajectron/utils/__init__.py @@ -0,0 +1,6 @@ +# SPDX-FileCopyrightText: (c) 2024 Tenstorrent AI ULC +# +# SPDX-License-Identifier: Apache-2.0 +from .trajectory_utils import prediction_output_to_trajectories +from .matrix_utils import block_diag, tile +from .os_utils import maybe_makedirs diff --git a/forge/test/models/pytorch/multimodal/trajectron/trajectron/utils/matrix_utils.py b/forge/test/models/pytorch/multimodal/trajectron/trajectron/utils/matrix_utils.py new file mode 100644 index 000000000..cb32abc44 --- /dev/null +++ b/forge/test/models/pytorch/multimodal/trajectron/trajectron/utils/matrix_utils.py @@ -0,0 +1,43 @@ +# SPDX-FileCopyrightText: (c) 2024 Tenstorrent AI ULC +# +# SPDX-License-Identifier: Apache-2.0 +import numpy as np +import torch + + +def attach_dim(v, n_dim_to_prepend=0, n_dim_to_append=0): + return v.reshape(torch.Size([1] * n_dim_to_prepend) + v.shape + torch.Size([1] * n_dim_to_append)) + + +def block_diag(m): + """ + Make a block diagonal matrix along dim=-3 + EXAMPLE: + block_diag(torch.ones(4,3,2)) + should give a 12 x 8 matrix with blocks of 3 x 2 ones. + Prepend batch dimensions if needed. + You can also give a list of matrices. + :type m: torch.Tensor, list + :rtype: torch.Tensor + """ + if type(m) is list: + m = torch.cat([m1.unsqueeze(-3) for m1 in m], -3) + + d = m.dim() + n = m.shape[-3] + siz0 = m.shape[:-3] + siz1 = m.shape[-2:] + m2 = m.unsqueeze(-2) + eye = attach_dim(torch.eye(n, device=m.device).unsqueeze(-2), d - 3, 1) + return (m2 * eye).reshape(siz0 + torch.Size(torch.tensor(siz1) * n)) + + +def tile(a, dim, n_tile, device="cpu"): + init_dim = a.size(dim) + repeat_idx = [1] * a.dim() + repeat_idx[dim] = n_tile + a = a.repeat(*(repeat_idx)) + order_index = torch.LongTensor(np.concatenate([init_dim * np.arange(n_tile) + i for i in range(init_dim)])).to( + device + ) + return torch.index_select(a, dim, order_index) diff --git a/forge/test/models/pytorch/multimodal/trajectron/trajectron/utils/os_utils.py b/forge/test/models/pytorch/multimodal/trajectron/trajectron/utils/os_utils.py new file mode 100644 index 000000000..5acd68d77 --- /dev/null +++ b/forge/test/models/pytorch/multimodal/trajectron/trajectron/utils/os_utils.py @@ -0,0 +1,19 @@ +# SPDX-FileCopyrightText: (c) 2024 Tenstorrent AI ULC +# +# SPDX-License-Identifier: Apache-2.0 +import os + + +def maybe_makedirs(path_to_create): + """This function will create a directory, unless it exists already, + at which point the function will return. + The exception handling is necessary as it prevents a race condition + from occurring. + Inputs: + path_to_create - A string path to a directory you'd like created. + """ + try: + os.makedirs(path_to_create) + except OSError: + if not os.path.isdir(path_to_create): + raise diff --git a/forge/test/models/pytorch/multimodal/trajectron/trajectron/utils/trajectory_utils.py b/forge/test/models/pytorch/multimodal/trajectron/trajectron/utils/trajectory_utils.py new file mode 100644 index 000000000..588151c6e --- /dev/null +++ b/forge/test/models/pytorch/multimodal/trajectron/trajectron/utils/trajectory_utils.py @@ -0,0 +1,46 @@ +# SPDX-FileCopyrightText: (c) 2024 Tenstorrent AI ULC +# +# SPDX-License-Identifier: Apache-2.0 +import numpy as np + + +def prediction_output_to_trajectories(prediction_output_dict, dt, max_h, ph, map=None, prune_ph_to_future=False): + + prediction_timesteps = prediction_output_dict.keys() + + output_dict = dict() + histories_dict = dict() + futures_dict = dict() + + for t in prediction_timesteps: + histories_dict[t] = dict() + output_dict[t] = dict() + futures_dict[t] = dict() + prediction_nodes = prediction_output_dict[t].keys() + for node in prediction_nodes: + predictions_output = prediction_output_dict[t][node] + position_state = {"position": ["x", "y"]} + + history = node.get(np.array([t - max_h, t]), position_state) # History includes current pos + history = history[~np.isnan(history.sum(axis=1))] + + future = node.get(np.array([t + 1, t + ph]), position_state) + future = future[~np.isnan(future.sum(axis=1))] + + if prune_ph_to_future: + predictions_output = predictions_output[:, :, : future.shape[0]] + if predictions_output.shape[2] == 0: + continue + + trajectory = predictions_output + + if map is None: + histories_dict[t][node] = history + output_dict[t][node] = trajectory + futures_dict[t][node] = future + else: + histories_dict[t][node] = map.to_map_points(history) + output_dict[t][node] = map.to_map_points(trajectory) + futures_dict[t][node] = map.to_map_points(future) + + return output_dict, histories_dict, futures_dict diff --git a/forge/test/models/pytorch/multimodal/trajectron/trajectron/visualization/__init__.py b/forge/test/models/pytorch/multimodal/trajectron/trajectron/visualization/__init__.py new file mode 100644 index 000000000..d8b5b2027 --- /dev/null +++ b/forge/test/models/pytorch/multimodal/trajectron/trajectron/visualization/__init__.py @@ -0,0 +1,5 @@ +# SPDX-FileCopyrightText: (c) 2024 Tenstorrent AI ULC +# +# SPDX-License-Identifier: Apache-2.0 +from .visualization import visualize_prediction, visualize_distribution +from .visualization_utils import plot_boxplots diff --git a/forge/test/models/pytorch/multimodal/trajectron/trajectron/visualization/visualization.py b/forge/test/models/pytorch/multimodal/trajectron/trajectron/visualization/visualization.py new file mode 100644 index 000000000..5c1547ef1 --- /dev/null +++ b/forge/test/models/pytorch/multimodal/trajectron/trajectron/visualization/visualization.py @@ -0,0 +1,137 @@ +# SPDX-FileCopyrightText: (c) 2024 Tenstorrent AI ULC +# +# SPDX-License-Identifier: Apache-2.0 +from utils import prediction_output_to_trajectories +from scipy import linalg +import matplotlib.pyplot as plt +import matplotlib.patches as patches +import matplotlib.patheffects as pe +import numpy as np +import seaborn as sns + + +def plot_trajectories( + ax, + prediction_dict, + histories_dict, + futures_dict, + line_alpha=0.7, + line_width=0.2, + edge_width=2, + circle_edge_width=0.5, + node_circle_size=0.3, + batch_num=0, + kde=False, +): + + cmap = ["k", "b", "y", "g", "r"] + + for node in histories_dict: + history = histories_dict[node] + future = futures_dict[node] + predictions = prediction_dict[node] + + if np.isnan(history[-1]).any(): + continue + + ax.plot(history[:, 0], history[:, 1], "k--") + + for sample_num in range(prediction_dict[node].shape[1]): + + if kde and predictions.shape[1] >= 50: + line_alpha = 0.2 + for t in range(predictions.shape[2]): + sns.kdeplot( + predictions[batch_num, :, t, 0], + predictions[batch_num, :, t, 1], + ax=ax, + shade=True, + shade_lowest=False, + color=np.random.choice(cmap), + alpha=0.8, + ) + + ax.plot( + predictions[batch_num, sample_num, :, 0], + predictions[batch_num, sample_num, :, 1], + color=cmap[node.type.value], + linewidth=line_width, + alpha=line_alpha, + ) + + ax.plot( + future[:, 0], + future[:, 1], + "w--", + path_effects=[pe.Stroke(linewidth=edge_width, foreground="k"), pe.Normal()], + ) + + # Current Node Position + circle = plt.Circle( + (history[-1, 0], history[-1, 1]), + node_circle_size, + facecolor="g", + edgecolor="k", + lw=circle_edge_width, + zorder=3, + ) + ax.add_artist(circle) + + ax.axis("equal") + + +def visualize_prediction(ax, prediction_output_dict, dt, max_hl, ph, robot_node=None, map=None, **kwargs): + + prediction_dict, histories_dict, futures_dict = prediction_output_to_trajectories( + prediction_output_dict, dt, max_hl, ph, map=map + ) + + assert len(prediction_dict.keys()) <= 1 + if len(prediction_dict.keys()) == 0: + return + ts_key = list(prediction_dict.keys())[0] + + prediction_dict = prediction_dict[ts_key] + histories_dict = histories_dict[ts_key] + futures_dict = futures_dict[ts_key] + + if map is not None: + ax.imshow(map.as_image(), origin="lower", alpha=0.5) + plot_trajectories(ax, prediction_dict, histories_dict, futures_dict, *kwargs) + + +def visualize_distribution(ax, prediction_distribution_dict, map=None, pi_threshold=0.05, **kwargs): + if map is not None: + ax.imshow(map.as_image(), origin="lower", alpha=0.5) + + for node, pred_dist in prediction_distribution_dict.items(): + if pred_dist.mus.shape[:2] != (1, 1): + return + + means = pred_dist.mus.squeeze().cpu().numpy() + covs = pred_dist.get_covariance_matrix().squeeze().cpu().numpy() + pis = pred_dist.pis_cat_dist.probs.squeeze().cpu().numpy() + + for timestep in range(means.shape[0]): + for z_val in range(means.shape[1]): + mean = means[timestep, z_val] + covar = covs[timestep, z_val] + pi = pis[timestep, z_val] + + if pi < pi_threshold: + continue + + v, w = linalg.eigh(covar) + v = 2.0 * np.sqrt(2.0) * np.sqrt(v) + u = w[0] / linalg.norm(w[0]) + + # Plot an ellipse to show the Gaussian component + angle = np.arctan(u[1] / u[0]) + angle = 180.0 * angle / np.pi # convert to degrees + ell = patches.Ellipse( + mean, v[0], v[1], 180.0 + angle, color="blue" if node.type.name == "VEHICLE" else "orange" + ) + ell.set_edgecolor(None) + ell.set_clip_box(ax.bbox) + ell.set_alpha(pi / 10) + ax.add_artist(ell) diff --git a/forge/test/models/pytorch/multimodal/trajectron/trajectron/visualization/visualization_utils.py b/forge/test/models/pytorch/multimodal/trajectron/trajectron/visualization/visualization_utils.py new file mode 100644 index 000000000..a12b8a2eb --- /dev/null +++ b/forge/test/models/pytorch/multimodal/trajectron/trajectron/visualization/visualization_utils.py @@ -0,0 +1,30 @@ +# SPDX-FileCopyrightText: (c) 2024 Tenstorrent AI ULC +# +# SPDX-License-Identifier: Apache-2.0 +import numpy as np +import pandas as pd +import seaborn as sns + + +def plot_boxplots(ax, perf_dict_for_pd, x_label, y_label): + perf_df = pd.DataFrame.from_dict(perf_dict_for_pd) + our_mean_color = sns.color_palette("muted")[9] + marker_size = 7 + mean_markers = "X" + with sns.color_palette("muted"): + sns.boxplot(x=x_label, y=y_label, data=perf_df, ax=ax, showfliers=False) + ax.plot( + [0], + [np.mean(perf_df[y_label])], + color=our_mean_color, + marker=mean_markers, + markeredgecolor="#545454", + markersize=marker_size, + zorder=10, + ) + + +def plot_barplots(ax, perf_dict_for_pd, x_label, y_label): + perf_df = pd.DataFrame.from_dict(perf_dict_for_pd) + with sns.color_palette("muted"): + sns.barplot(x=x_label, y=y_label, ax=ax, data=perf_df)