From a5257972d062586beb35d01e5f51cdf7f8e7a75e Mon Sep 17 00:00:00 2001
From: Andi Gerken <andi.gerken@gmail.com>
Date: Mon, 12 Apr 2021 21:12:55 +0200
Subject: [PATCH] Bugfix, in previous versions, the poses_calc_ori was shifted.

---
 src/robofish/evaluate/evaluate.py |  1 +
 src/robofish/io/entity.py         | 37 +++++++++++++++++++------------
 src/robofish/io/file.py           |  4 ++++
 tests/robofish/io/test_entity.py  | 10 ++++-----
 4 files changed, 33 insertions(+), 19 deletions(-)

diff --git a/src/robofish/evaluate/evaluate.py b/src/robofish/evaluate/evaluate.py
index 6670645..1a97133 100644
--- a/src/robofish/evaluate/evaluate.py
+++ b/src/robofish/evaluate/evaluate.py
@@ -123,6 +123,7 @@ def evaluate_turn(
     for k, files in enumerate(files_per_path):
         path_turns = []
         for p, file in files.items():
+            # TODO: Use new io functions (entity_poses_calc_ori_rad)
             poses = file.select_entity_poses(
                 None if predicate is None else predicate[k]
             )
diff --git a/src/robofish/io/entity.py b/src/robofish/io/entity.py
index e407989..3759cea 100644
--- a/src/robofish/io/entity.py
+++ b/src/robofish/io/entity.py
@@ -138,17 +138,22 @@ class Entity(h5py.Group):
         return self["orientations"]
 
     @property
-    def orientations_calculated(self):
+    def poses_calc_ori_rad(self):
+        # Diff between positions [t - 1, 2]
         diff = np.diff(self.positions, axis=0)
-        angles = np.arctan2(diff[:, 1], diff[:, 0])
-        return angles[:, np.newaxis]
 
-    @property
-    def poses_calc_ori_rad(self):
-        return np.concatenate(
-            [self.positions[:-1], self.orientations_calculated], axis=1
+        # angles [t - 1]
+        angles = utils.limit_angle_range(
+            np.arctan2(diff[:, 1], diff[:, 0]), _range=(0, 2 * np.pi)
+        )
+
+        # Positions with angles. The first position is cut of, as it does not have an orientation.
+        poses_with_calculated_orientation = np.concatenate(
+            [self.positions[1:], angles[:, np.newaxis]], axis=1
         )
 
+        return poses_with_calculated_orientation
+
     @property
     def poses(self):
         return np.concatenate([self.positions, self.orientations], axis=1)
@@ -157,7 +162,9 @@ class Entity(h5py.Group):
     def poses_rad(self):
         poses = self.poses
         # calculate the angles from the orientation vectors, write them to the third row and delete the fourth row
-        ori_rad = np.arctan2(poses[:, 3], poses[:, 2])
+        ori_rad = utils.limit_angle_range(
+            np.arctan2(poses[:, 3], poses[:, 2]), _range=(0, 2 * np.pi)
+        )
         return np.concatenate([poses[:, :2], ori_rad[:, np.newaxis]], axis=1)
 
     @property
@@ -173,10 +180,12 @@ class Entity(h5py.Group):
             We assume, that the entity is oriented "correctly" in the first pose. So the first turn angle is 0.
         """
 
-        diff = np.diff(self.positions, axis=0)
-        speed = np.linalg.norm(diff, axis=1)
-        angles = np.arctan2(diff[:, 1], diff[:, 0])
-        turn = np.zeros_like(angles)
-        turn[0] = 0
-        turn[1:] = utils.limit_angle_range(np.diff(angles))
+        # poses with calulated orientation have first position cut of as it does not have an orientation
+        # (t - 1, (x ,y, ori))
+        poses_calc_ori = self.poses_calc_ori_rad
+
+        # Differences cuts of last item (t - 2, (dx, dy, d ori))
+        diff = np.diff(poses_calc_ori, axis=0)
+        speed = np.linalg.norm(diff[:, :2], axis=1)
+        turn = utils.limit_angle_range(diff[:, 2], _range=(-np.pi, np.pi))
         return np.stack([speed, turn], axis=-1)
diff --git a/src/robofish/io/file.py b/src/robofish/io/file.py
index 9ff216c..616eb92 100644
--- a/src/robofish/io/file.py
+++ b/src/robofish/io/file.py
@@ -143,6 +143,8 @@ class File(h5py.File):
         Args:
             path: path to a io file as a string or path object. If no path is specified, the last known path (from loading or saving) is used.
             strict_validate: optional boolean, if the file should be strictly validated, before saving. The default is True.
+        Returns:
+            The file itself, so something like f = robofish.io.File().save_as("file.hdf5") works
         """
 
         self.validate(strict_validate=strict_validate)
@@ -155,6 +157,8 @@ class File(h5py.File):
 
         shutil.copyfile(Path(self.filename).resolve(), path)
 
+        return self
+
     def create_sampling(
         self,
         name: str = None,
diff --git a/tests/robofish/io/test_entity.py b/tests/robofish/io/test_entity.py
index 7427f5a..17fdcd2 100644
--- a/tests/robofish/io/test_entity.py
+++ b/tests/robofish/io/test_entity.py
@@ -40,13 +40,13 @@ def test_entity_turn_speed():
     )
     e = f.create_entity("fish", positions=positions)
     speed_turn = e.speed_turn
-    assert speed_turn.shape == (99, 2)
+    assert speed_turn.shape == (98, 2)
 
-    # No turn in the first timestep, since initialization turns it the right way
-    assert speed_turn[0, 1] == 0
+    # Turns and speeds shoud be all the same afterwards, since the fish swims with constant velocity and angular velocity.
+    assert (np.std(speed_turn, axis=0) < 0.0001).all()
 
-    # Turns and speeds shoud afterwards be all the same afterwards, since the fish swims with constant velocity and angular velocity.
-    assert (np.std(speed_turn[1:], axis=0) < 0.0001).all()
+    # Cut off the first position as it cannot be simulated
+    positions = positions[1:]
 
     # Use turn_speed to generate positions
     gen_positions = np.zeros((positions.shape[0], 3))
-- 
GitLab