diff --git a/PyTorch Model/model_functions.py b/PyTorch Model/model_functions.py
index 9cca817c898477221322bb93a94b44dda4273cbb..e8a8af6a9988aeba91d4f3753fd0f12aa9d25c97 100644
--- a/PyTorch Model/model_functions.py	
+++ b/PyTorch Model/model_functions.py	
@@ -12,42 +12,70 @@ from torch.optim import lr_scheduler
 
 from data_loader import get_dataloader
 import models
-from loss import GeoPoseLoss
+from losses import GeoPoseLoss
 from pose_utils import *
 
 
+def save_checkpoint(epoch, model, optimizer, criterion):  # for sx and sq
+    filename = os.path.join('posenet.pth.tar'.format(epoch))
+    checkpoint_dict = \
+        {'epoch': epoch, 'model_state_dict': model.state_dict(),
+         'optim_state_dict': optimizer.state_dict(),
+         'criterion_state_dict': criterion.state_dict()}
+    torch.save(checkpoint_dict, filename)
+
+
+def load_checkpoint(model, criterion, optimizer, filename):
+    if os.path.isfile(filename):
+        checkpoint = torch.load(filename)
+        model.load_state_dict(checkpoint['model_state_dict'])
+        optimizer.load_state_dict(checkpoint['optim_state_dict'])
+        criterion.load_state_dict(checkpoint['criterion_state_dict'])
+        epoch = checkpoint['epoch']
+        print("Checkpoint loaded from epoch: ", epoch)
+        return epoch
+    else:
+        print("No checkpoint found at", filename)
+        return 0  # for start epoch
+
+
 def train_model(config):
     cudnn.benchmark = True
     device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
     print("Using device: ", device)
-    # TODO set seed
-    # TODO print out config
+    torch.manual_seed(7)
 
-    # DATA
+    ### DATA
     # dataset_path = './datasets/KingsCollege'
     data_name = config.dataset_path.split('/')[-1]
     model_save_path = 'models_%s' % data_name
     tb_save_path = 'runs_%s' % data_name
 
+    print(config)
+
     train_loader = get_dataloader(dataset_path=config.dataset_path, mode='train',
                                   model=config.model, batch_size=config.batch_size)
     val_loader = get_dataloader(dataset_path=config.dataset_path, mode='val',
                                 model=config.model, batch_size=config.batch_size)
+    print(f"Loading data from: {config.dataset_path}")
+    print(f"No. of Training samples: {len(train_loader)*config.batch_size}; {len(train_loader)} batches of {config.batch_size}")
+    print(f"No. of Validation samples: {len(val_loader)*config.batch_size}; {len(val_loader)} batches of {config.batch_size}")
 
-    # MODEL
+    ### MODEL
     if config.model == 'googlenet':
         model = models.GoogleNet(fixed_weight=config.fixed_weight, dropout_rate=config.dropout_rate)
     else:
-        model = models.ResNet(fixed_weight=config.fixed_weight, dropout_rate=config.dropout_rate)
+        # model = models.ResNet(fixed_weight=config.fixed_weight, dropout_rate=config.dropout_rate)
+        model = models.PoseNet()
     model.to(device)
 
-    if config.pretrained_model:  # TODO either none or the path and epoch to start from
-        model_path = '/%s_net.pth' % config.pretrained_model
+    if config.pretrained_model:
+        model_path = config.pretrained_model
         model.load_state_dict(torch.load(model_path))
-        print('Load pretrained network: ', model_path)
+        print('Loading pretrained network from: ', model_path)
 
-    # LOSS AND OPTIMISER
-    criterion = GeoPoseLoss(config.learn_beta)
+    ### LOSS AND OPTIMISER
+    criterion = GeoPoseLoss(learn_beta=config.learn_beta)
     criterion.to(device)
 
     if config.learn_beta:
@@ -63,7 +91,7 @@ def train_model(config):
     # LR is decayed by the value of gamma
     scheduler = lr_scheduler.StepLR(optimizer, step_size=config.num_epochs_decay, gamma=0.1)
 
-    # TRAINING
+    ### TRAINING
     num_epochs = config.num_epochs
 
     if not os.path.exists(model_save_path):
@@ -72,13 +100,23 @@ def train_model(config):
     # tensorboard
     if not os.path.exists(tb_save_path):
         os.makedirs(tb_save_path)
+    with open(tb_save_path + '/config.txt', mode="w+") as f:
+        f.write(str(config))
     writer = SummaryWriter(log_dir=tb_save_path)
 
     start_time = time.time()
     n_iter = 0  # total no of batches over all epochs, useful for logger
+    n_val_iter = 0
+    if config.pretrained_model:
+        n_iter = int(config.pretrained_epoch) * len(train_loader)  # provided batch size is same
+        n_val_iter = int(config.pretrained_epoch) * len(val_loader)
     start_epoch = 0
     if config.pretrained_model:
-        start_epoch = int(config.pretrained_model)
+        start_epoch = int(config.pretrained_epoch)
+
+    # IF CHECKPOINT PRESENT, LOAD: - THIS IS ONLY FOR CONDOR
+    # start_epoch = load_checkpoint(model, criterion, optimizer, 'posenet.pth.tar')
+    # n_iter = int(start_epoch) * len(train_loader)
 
     for epoch in range(start_epoch, num_epochs):
         print('Epoch {}/{}'.format(epoch + 1, num_epochs))
@@ -109,13 +147,12 @@ def train_model(config):
             # print("ACT POSE", pos_true, "ACT ORI", ori_true)
 
             ori_out = F.normalize(ori_out, p=2, dim=1)
-            ori_true = F.normalize(ori_true, p=2, dim=1)
+            ori_true = F.normalize(ori_true, p=2, dim=1)  # doesn't make any difference
 
             loss, loss_pos_print, loss_ori_print = criterion(pos_out, ori_out, pos_true, ori_true)
             # print("LOSS", loss.item())
             loss_print = loss.item()
-
-            # loss = loss_pos + beta * loss_ori
+            del inputs, poses
 
             # TODO use config.log_step
             error_train.append(loss_print)
@@ -160,26 +197,32 @@ def train_model(config):
 
             loss, loss_pos_print, loss_ori_print = criterion(pos_out, ori_out, pos_true, ori_true)
             loss_print = loss.item()
+            del inputs, poses
 
             error_val.append(loss_print)
-            # TODO log val loss
+            writer.add_scalar('loss/overall_val_loss', loss_print, n_val_iter)
+            n_val_iter += 1
+
             print('batch#{} val Loss: total loss {:.3f} / pos loss {:.3f} / ori loss {:.3f}'.format(i, loss_print,
                                                                                               loss_pos_print,
                                                                                                loss_ori_print))
 
         # END OF EPOCH
-        error_train_loss = np.median(error_train)
-        error_val_loss = np.median(error_val)
+        error_train_avg = np.mean(error_train)
+        error_val_avg = np.mean(error_val)
 
-        print('Overall Train and Validation loss for epoch {} / {}'.format(error_train_loss, error_val_loss))
+        print('Overall Train and Validation loss for epoch {} / {}'.format(error_train_avg, error_val_avg))
         print('=' * 40)
         print('=' * 40)
 
-        writer.add_scalars('loss/trainval', {'train': error_train_loss, 'val': error_val_loss}, epoch+1)
+        writer.add_scalars('loss/trainval', {'train': np.median(error_train_avg), 'val': np.median(error_val_avg)}, epoch+1)
 
         if (epoch + 1) % config.model_save_step == 0:
-            # save_filename = model_save_path + '/%s_net.pth' % epoch
-            save_filename = model_save_path + '/posenet.pth'
+            # save_checkpoint(epoch=epoch, model=model.cpu(), optimizer=optimizer, criterion=criterion.cpu())
+            # if torch.cuda.is_available():
+            #     model.to(device)
+            #     criterion.to(device)
+            save_filename = model_save_path + '/posenet_{}.pth'.format(epoch+1)
             torch.save(model.cpu().state_dict(), save_filename)
             if torch.cuda.is_available():
                 model.to(device)
@@ -210,19 +253,27 @@ def test_model(config):
     device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
     print("Using device: ", device)
 
-    # LOAD THE MODEL
+    # DATA
     data_name = config.dataset_path.split('/')[-1]
     model_save_path = 'models_%s' % data_name
     tb_save_path = 'runs_%s' % data_name
-    # f = open(tb_save_path + '/test_result.csv', 'w+')
+    f = open(tb_save_path + '/test_result.csv', 'w+')
+
+    test_loader = get_dataloader(dataset_path=config.dataset_path, mode='test')  # BATCH SIZE IS 1
+    print(f"Loading data from: {config.dataset_path}")
+    print(f"No. of Test samples: {len(test_loader)}")
 
+    # LOAD THE MODEL
     if config.pretrained_model:
         model_save_path = config.pretrained_model
+    else:
+        print("No model specified")  # TODO load best
 
     if config.model == 'googlenet':
-        model = models.GoogleNet(fixed_weight=config.fixed_weight, dropout_rate=config.dropout_rate)
+        model = models.GoogleNet()
     else:
-        model = models.ResNet(fixed_weight=config.fixed_weight, dropout_rate=config.dropout_rate)
+        # model = models.ResNet()
+        model = models.PoseNet()
     model.to(device)
 
     print('Loading pretrained model: ', model_save_path)
@@ -230,17 +281,16 @@ def test_model(config):
 
     model.eval()
 
+    # arrays to store individual losses
+    pos_loss = []
+    ori_loss = []
     total_pos_loss = 0
     total_ori_loss = 0
-    # arrays to store individual losses
-    pos_loss_arr = []
-    ori_loss_arr = []
+    # for plotting later
     true_pose_list = []
     estim_pose_list = []
 
-    test_loader = get_dataloader(dataset_path=config.dataset_path, mode='test')  # BATCH SIZE IS 1
     for i, (inputs, poses) in enumerate(test_loader):
-        print(i)
 
         inputs = inputs.to(device)
         pos_out, ori_out = model(inputs)
@@ -249,49 +299,53 @@ def test_model(config):
         # Then, detach the tensor from the computation graph using detach()
         pos_out = pos_out.squeeze(0).detach().cpu().numpy()
         ori_out = F.normalize(ori_out, p=2, dim=1)
-        ori_out = quat_to_euler(ori_out.squeeze(0).detach().cpu().numpy())
+        # ori_out = quat_to_euler(ori_out.squeeze(0).detach().cpu().numpy())
+        ori_out = ori_out.squeeze(0).detach().cpu().numpy()
         print('pos out', pos_out)
         print('ori_out', ori_out)
 
         pos_true = poses[:, :3].squeeze(0).numpy()
         ori_true = poses[:, 3:].squeeze(0).numpy()
-
-        ori_true = quat_to_euler(ori_true)
+        # ori_true = quat_to_euler(ori_true)
         print('pos true', pos_true)
         print('ori true', ori_true)
-        # l2 norm
-        loss_pos_print = array_dist(pos_out, pos_true)
-        loss_ori_print = array_dist(ori_out, ori_true)
 
-        true_pose_list.append(np.hstack((pos_true, ori_true)))
-        estim_pose_list.append(np.hstack((pos_out, ori_out)))
+        # l2 distance
+        loss_pos_print = position_dist(pos_out, pos_true)
+        loss_ori_print = rotation_dist(ori_out, ori_true)
+
+        pos_loss.append(loss_pos_print)
+        ori_loss.append(loss_ori_print)
 
         total_pos_loss += loss_pos_print
         total_ori_loss += loss_ori_print
 
-        pos_loss_arr.append(loss_pos_print)
-        ori_loss_arr.append(loss_ori_print)
-        print('{}th Error: pos error {:.3f} / ori error {:.3f}'.format(i, loss_pos_print, loss_ori_print))
+        true_pose_list.append(np.hstack((pos_true, ori_true)))
+        estim_pose_list.append(np.hstack((pos_out, ori_out)))
 
-    position_error = np.median(pos_loss_arr)
-    rotation_error = np.median(ori_loss_arr)
+        print('batch#{} error: pos error {:.3f} / ori error {:.3f}'.format(i, loss_pos_print, loss_ori_print))
+        f.write('batch#{} error: pos error {:.3f} / ori error {:.3f} \n'.format(i, loss_pos_print, loss_ori_print))
 
+    # END OF EPOCH
     print('=' * 20)
-    print('Overall median pose error {:.3f}m / {:.3f}rad'.format(position_error, rotation_error))
-    print('Overall average pose error {:.3f}m / {:.3f}rad'.format(np.mean(pos_loss_arr), np.mean(ori_loss_arr)))
-    # f.close()
+    print('Overall median pose error {:.3f}m / {:.3f}o'.format(np.median(pos_loss), np.median(ori_loss)))
+    f.write('Overall median pose error {:.3f}m / {:.3f}o \n'.format(np.median(pos_loss), np.median(ori_loss)))
+    print('Overall average pose error {:.3f}m / {:.3f}o'.format(np.mean(pos_loss), np.mean(ori_loss)))
+    f.write('Overall average pose error {:.3f}m / {:.3f}o \n'.format(np.mean(pos_loss), np.mean(ori_loss)))
 
     f_true = tb_save_path + '/pose_true.csv'
     f_estim = tb_save_path + '/pose_estim.csv'
     np.savetxt(f_true, true_pose_list, delimiter=',')
     np.savetxt(f_estim, estim_pose_list, delimiter=',')
 
+    f.close()
+
 
 if __name__ == '__main__':
     # train_model(save_name='KingsCollege-ResNet')
     # test_model(save_name='KingsCollege-ResNet')
 
     # train_model(save_name='KingsCollege-GNet')
-    train_model(save_name='KingsCollege-ResNet-V')
+    # train_model(save_name='KingsCollege-ResNet-V')
     # test_model(save_name='KingsCollege-ResNet-V')
-
+    pass
diff --git a/PyTorch Model/models.py b/PyTorch Model/models.py
index f324adecdec1ad605d3dbd6aa9c91544eea3e71e..7f8bc4919b3d90d8f2e5cd33a650100f5007985a 100644
--- a/PyTorch Model/models.py	
+++ b/PyTorch Model/models.py	
@@ -6,6 +6,48 @@ from torchvision import models
 from torchsummary import summary
 
 
+# https://github.com/NVlabs/geomapnet/blob/master/models/posenet.py
+class PoseNet(nn.Module):
+    def __init__(self, droprate=0.5, pretrained=True):
+        super(PoseNet, self).__init__()
+        self.droprate = droprate
+
+        # replace the last FC layer in feature extractor
+        self.feature_extractor = models.resnet34(weights="IMAGENET1K_V1")
+        self.feature_extractor.avgpool = nn.AdaptiveAvgPool2d(1)
+        in_dim = self.feature_extractor.fc.in_features
+        self.feature_extractor.fc = nn.Linear(in_dim, 2048)
+
+        self.fc_xyz = nn.Linear(2048, 3)
+        self.fc_wpqr = nn.Linear(2048, 4)
+
+        # initialize
+        if pretrained:
+            init_modules = [self.feature_extractor.fc, self.fc_xyz, self.fc_wpqr]
+        else:
+            init_modules = self.modules()
+
+        for m in init_modules:
+            if isinstance(m, nn.Conv2d) or isinstance(m, nn.Linear):
+                nn.init.kaiming_normal_(m.weight.data)
+                if m.bias is not None:
+                    nn.init.constant_(m.bias.data, 0)
+
+    def forward(self, x):
+        x = self.feature_extractor(x)
+        # 1 x 2048
+        x = F.relu(x)
+        if self.droprate > 0:
+            x = F.dropout(x, p=self.droprate)
+
+        xyz = self.fc_xyz(x)
+        wpqr = self.fc_wpqr(x)
+        return xyz, wpqr
+    # seems to be no random crop, no colour jitter;
+    # log of quaternion
+    # 300 epochs
+
+
 class ResNet(nn.Module):
     def __init__(self, fixed_weight=False, dropout_rate=0.0):
         super(ResNet, self).__init__()
@@ -38,7 +80,7 @@ class ResNet(nn.Module):
     def forward(self, x):
         x = self.base_model(x)
         # print("AFTER RESNET", x.shape)
-        x = x.view(x.size(0), -1)  # flatten
+        x = x.view(x.size(0), -1)  # flatten TODO global average pool
         # print("AFTER VIEW", x.shape)
         x = self.fc_last(x)
         x = F.relu(x)
@@ -111,127 +153,12 @@ class GoogleNet(nn.Module):
         return pos, ori
 
 
-class NetVLAD(nn.Module):
-    """NetVLAD layer implementation"""
-
-    def __init__(self, num_clusters=64, dim=128, alpha=100.0,
-                 normalize_input=True):
-        """
-        Args:
-            num_clusters : int
-                The number of clusters
-            dim : int
-                Dimension of descriptors
-            alpha : float
-                Parameter of initialization. Larger value is harder assignment.
-            normalize_input : bool
-                If true, descriptor-wise L2 normalization is applied to input.
-        """
-        super(NetVLAD, self).__init__()
-        self.num_clusters = num_clusters
-        self.dim = dim
-        self.alpha = alpha
-        self.normalize_input = normalize_input
-        self.conv = nn.Conv2d(dim, num_clusters, kernel_size=(1, 1), bias=True)
-        self.centroids = nn.Parameter(torch.rand(num_clusters, dim))  # cluster centres
-        self._init_params()
-
-    def _init_params(self):
-        # manually initialising rather than PyTorch assigning random values to weights and 0s to bias
-        self.conv.weight = nn.Parameter(
-            (2.0 * self.alpha * self.centroids).unsqueeze(-1).unsqueeze(-1)
-        )
-        self.conv.bias = nn.Parameter(
-            - self.alpha * self.centroids.norm(dim=1)
-        )
-
-    def forward(self, x):
-        N, C = x.shape[:2]  # N->batch size, C->channels
-
-        if self.normalize_input:
-            x = F.normalize(x, p=2, dim=1)  # across descriptor dim
-
-        # soft-assignment
-        soft_assign = self.conv(x).view(N, self.num_clusters, -1)
-        soft_assign = F.softmax(soft_assign, dim=1)
-
-        x_flatten = x.view(N, C, -1)
-
-        # calculate residuals to each clusters
-        residual = x_flatten.expand(self.num_clusters, -1, -1, -1).permute(1, 0, 2, 3) - \
-                   self.centroids.expand(x_flatten.size(-1), -1, -1).permute(1, 2, 0).unsqueeze(0)
-        residual *= soft_assign.unsqueeze(2)
-        vlad = residual.sum(dim=-1)
-
-        vlad = F.normalize(vlad, p=2, dim=2)  # intra-normalization
-        vlad = vlad.view(x.size(0), -1)  # flatten
-        vlad = F.normalize(vlad, p=2, dim=1)  # L2 normalize
-
-        return vlad
-
-
-class ResNetVLAD(nn.Module):
-    def __init__(self, fixed_weight=False):
-        super(ResNetVLAD, self).__init__()
-
-        # Discard layers at the end of base network
-        encoder = models.resnet34(weights="IMAGENET1K_V1")
-        self.base_model = nn.Sequential(
-            encoder.conv1,
-            encoder.bn1,
-            encoder.relu,
-            encoder.maxpool,
-            encoder.layer1,
-            encoder.layer2,
-            encoder.layer3,
-            encoder.layer4
-        )
-
-        if fixed_weight:
-            for param in self.base_model.parameters():
-                param.requires_grad = False
-
-        dim = list(self.base_model.parameters())[-1].shape[0]  # last channels (512)
-        self.net_vlad = NetVLAD(num_clusters=32, dim=dim, alpha=1.0)
-
-        self.fc_last = nn.Linear(16384, 2048, bias=True)
-        self.fc_position = nn.Linear(2048, 3, bias=True)
-        self.fc_rotation = nn.Linear(2048, 4, bias=True)
-
-        init_modules = [self.fc_last, self.fc_position, self.fc_rotation]
-
-        for module in init_modules:
-            if isinstance(module, nn.Conv2d) or isinstance(module, nn.Linear):
-                nn.init.kaiming_normal_(module.weight)
-                if module.bias is not None:
-                    nn.init.constant_(module.bias, 0)
-
-    def forward(self, x):
-        # print("FORWARD PASS STARTING", x.shape)
-        x = self.base_model(x)
-        # print("AFTER RESNET", x.shape)
-        embedded_x = self.net_vlad(x)
-        # print("AFTER NETVLAD", embedded_x.shape)
-
-        x = self.fc_last(embedded_x)
-        x = F.relu(x)
-
-        # from that space of 2048, 3 for pos and 4 for ori are sampled
-        position = self.fc_position(x)
-        rotation = self.fc_rotation(x)
-
-        return position, rotation
-
-
 if __name__ == '__main__':
+    device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
     # model = GoogleNet()
-    # model.to("cuda:0")
+    # model.to(device)
     # summary(model, (3, 299, 299))
 
-    # model2 = ResNet()
-    # model2.to("cuda:0")
-    # summary(model2, (3, 224, 224))
-
-    model3 = ResNetVLAD()
-    model3.to("cuda:0")
-    summary(model3, (3, 224, 224))
+    model2 = PoseNet()
+    model2.to(device)
+    summary(model2, (3, 224, 224))
diff --git a/PyTorch Model/plot_ori.py b/PyTorch Model/plot_ori.py
new file mode 100644
index 0000000000000000000000000000000000000000..1c7a2e14bc9c2e881fe260167d89d68c11f1a77b
--- /dev/null
+++ b/PyTorch Model/plot_ori.py	
@@ -0,0 +1,66 @@
+# Uncomment for 6-plot 2D image
+# import matplotlib
+# matplotlib.use('Agg')
+
+import pandas as pd
+import matplotlib.pyplot as plt
+from mpl_toolkits.mplot3d import Axes3D
+from scipy.spatial.transform import Rotation
+
+PATH = 'runs_TurtlebotTourCVSSP/run 3 - moar data'
+pose_true = pd.read_csv(PATH + '/pose_true.csv')
+pose_estim = pd.read_csv(PATH + '/pose_estim.csv')
+
+# fig = plt.figure(figsize=(15, 10))
+N = 25  # start from which test sample
+
+# read ori values
+for i in range(6):
+    true_quaternion = pose_true.iloc[N, 3:7].values
+    predicted_quaternion = pose_estim.iloc[N, 3:7].values
+    N = N+1
+
+    # Convert quaternions to rotation matrices (when multiplied with a vector produced the rotated vector
+    true_rotation = Rotation.from_quat(true_quaternion).as_matrix()
+    print(true_rotation)
+    predicted_rotation = Rotation.from_quat(predicted_quaternion).as_matrix()
+
+    # Extract x, y, z axes from rotation matrices
+    true_x_axis = true_rotation[:, 0]
+    true_y_axis = true_rotation[:, 1]
+    true_z_axis = true_rotation[:, 2]  # forward-facing or nose direction
+
+    predicted_x_axis = predicted_rotation[:, 0]
+    predicted_y_axis = predicted_rotation[:, 1]
+    predicted_z_axis = predicted_rotation[:, 2]
+
+    # Create a 3D plot
+    fig = plt.figure()
+    ax = fig.add_subplot(111, projection='3d')
+    # ax = fig.add_subplot(2, 3, i+1, projection='3d')
+
+    # Plot the true orientation (red axes)
+    ax.quiver(0, 0, 0, true_x_axis[0], true_x_axis[1], true_x_axis[2], color='r', linestyle='dashed')
+    ax.quiver(0, 0, 0, true_y_axis[0], true_y_axis[1], true_y_axis[2], color='r', linestyle='dotted')
+    ax.quiver(0, 0, 0, true_z_axis[0], true_z_axis[1], true_z_axis[2], color='r')
+
+    # Plot the predicted orientation (green axes)
+    ax.quiver(0, 0, 0, predicted_x_axis[0], predicted_x_axis[1], predicted_x_axis[2], color='g', linestyle='dashed')
+    ax.quiver(0, 0, 0, predicted_y_axis[0], predicted_y_axis[1], predicted_y_axis[2], color='g', linestyle='dotted')
+    ax.quiver(0, 0, 0, predicted_z_axis[0], predicted_z_axis[1], predicted_z_axis[2], color='g')
+
+    # Set plot limits and labels
+    ax.set_xlim([-1, 1])
+    ax.set_ylim([-1, 1])
+    ax.set_zlim([-1, 1])
+    ax.set_xlabel('X')
+    ax.set_ylabel('Y')
+    ax.set_zlabel('Z')
+    # ax.set_title('True vs Predicted Quaternion Orientations')
+
+    # ax.set_title(f'Subplot {i + 1}')
+    # plt.tight_layout()  # prevent overlapping titles and labels
+
+    # Show the plot
+    plt.show()
+    # plt.savefig(PATH + '/ori_subplots.png')
diff --git a/PyTorch Model/plot_poses.py b/PyTorch Model/plot_poses.py
new file mode 100644
index 0000000000000000000000000000000000000000..febb3d82ebb3eb35b9f91e26ba2b71723dfb4685
--- /dev/null
+++ b/PyTorch Model/plot_poses.py	
@@ -0,0 +1,32 @@
+import pandas as pd
+import numpy as np
+import matplotlib.pyplot as plt
+from mpl_toolkits.mplot3d import Axes3D
+
+PATH = 'runs_TurtlebotTourCVSSP/run 3 - moar data'
+pose_true = pd.read_csv(PATH + '/pose_true.csv')
+pose_estim = pd.read_csv(PATH + '/pose_estim.csv')
+
+# read ALL pos values
+N = len(pose_true)
+position_true = pose_true.iloc[:N, 0:3].values
+position_estim = pose_estim.iloc[:N, 0:3].values
+
+fig = plt.figure()
+ax = fig.add_subplot(111, projection='3d')  # Create a 3D subplot
+ax.set_xlabel('x')
+ax.set_ylabel('y')
+ax.set_zlabel('z')
+
+ax.scatter(position_true[:N, 0], position_true[:N, 1], position_true[:N, 2], c='r', marker='o', label='truth')
+ax.scatter(position_estim[:N, 0], position_estim[:N, 1], position_estim[:N, 2], c='b', marker='o', label='estimation')
+
+# # connect the true and estim points with green lines
+# for i in range(position_true.__len__()):
+#    position_set = np.vstack((position_true[i,:], position_estim[i,:]))
+#    ax.plot(position_set[:,0], position_set[:,1], position_set[:,2], color='green', linewidth=0.5)
+
+ax.legend()
+
+# plt.axis('scaled')
+plt.show()
diff --git a/PyTorch Model/pose_utils.py b/PyTorch Model/pose_utils.py
index 4161e53bb458a00ece9c819f032b689be46ecd6b..ac3126ab41ca94b6e32280d9287ee19a811a8694 100644
--- a/PyTorch Model/pose_utils.py	
+++ b/PyTorch Model/pose_utils.py	
@@ -2,7 +2,7 @@ import torch
 import numpy as np
 
 
-def quat_to_euler(q, is_degree=False):
+def quat_to_euler(q, is_degree=True):
     w, x, y, z = q[0], q[1], q[2], q[3]
 
     t0 = +2.0 * (w * x + y * z)
@@ -26,21 +26,26 @@ def quat_to_euler(q, is_degree=False):
     return np.array([roll, pitch, yaw])
 
 
-def array_dist(pred, target):
-    return np.linalg.norm(pred - target, 2)
+def position_dist(pred, target):
+    return np.linalg.norm(pred-target, ord=2)
 
 
-def position_dist(pred, target):
-    return np.linalg.norm(pred-target, 2)
+def rotation_dist(pred, target):  # angle-axis
+
+    # Calculate quaternion difference
+    # scalar dot product by element-wise multiplying each element, then adding
+    quaternion_difference = np.dot(target, pred)
 
+    # Calculate rotation error in radians
+    # arc cosine (inverse cosine) is often used in trigonometric calculations, and in this context,
+    # it is used to compute the rotation angle based on the quaternion difference.
+    alpha = 2 * np.arccos(np.abs(quaternion_difference))
 
-def rotation_dist(pred, target):
-    pred = quat_to_euler(pred)
-    target = quat_to_euler(target)
+    # Convert radians to degrees
+    return alpha * (180.0 / np.pi)
 
-    return np.linalg.norm(pred-target, 2)
 
-# only for bayesian
+# only for bayesian posenet
 def fit_gaussian(pose_quat):
     # pose_quat = pose_quat.detach().cpu().numpy()